summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xexamples/disco_browser.py198
-rwxr-xr-xexamples/echo_client.py7
-rw-r--r--setup.py4
-rw-r--r--sleekxmpp/basexmpp.py99
-rw-r--r--sleekxmpp/plugins/__init__.py6
-rw-r--r--sleekxmpp/plugins/gmail_notify.py2
-rw-r--r--sleekxmpp/plugins/jobs.py3
-rw-r--r--sleekxmpp/plugins/xep_0004.py3
-rw-r--r--sleekxmpp/plugins/xep_0030/disco.py366
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/disco.py0
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/items.py4
-rw-r--r--sleekxmpp/plugins/xep_0030/static.py49
-rw-r--r--sleekxmpp/plugins/xep_0045.py648
-rw-r--r--sleekxmpp/plugins/xep_0050.py2
-rw-r--r--sleekxmpp/plugins/xep_0060.py30
-rw-r--r--sleekxmpp/plugins/xep_0092.py2
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py23
17 files changed, 994 insertions, 452 deletions
diff --git a/examples/disco_browser.py b/examples/disco_browser.py
new file mode 100755
index 00000000..0d746083
--- /dev/null
+++ b/examples/disco_browser.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import sys
+import time
+import logging
+import getpass
+from optparse import OptionParser
+
+import sleekxmpp
+
+
+# Python versions before 3.0 do not use UTF-8 encoding
+# by default. To ensure that Unicode is handled properly
+# throughout SleekXMPP, we will set the default encoding
+# ourselves to UTF-8.
+if sys.version_info < (3, 0):
+ reload(sys)
+ sys.setdefaultencoding('utf8')
+
+
+class Disco(sleekxmpp.ClientXMPP):
+
+ """
+ A demonstration for using basic service discovery.
+
+ Send a disco#info and disco#items request to a JID/node combination,
+ and print out the results.
+
+ May also request only particular info categories such as just features,
+ or just items.
+ """
+
+ def __init__(self, jid, password, target_jid, target_node='', get=''):
+ sleekxmpp.ClientXMPP.__init__(self, jid, password)
+
+ # Using service discovery requires the XEP-0030 plugin.
+ self.register_plugin('xep_0030')
+
+ self.get = get
+ self.target_jid = target_jid
+ self.target_node = target_node
+
+ # Values to control which disco entities are reported
+ self.info_types = ['', 'all', 'info', 'identities', 'features']
+ self.identity_types = ['', 'all', 'info', 'identities']
+ self.feature_types = ['', 'all', 'info', 'features']
+ self.items_types = ['', 'all', 'items']
+
+
+ # The session_start event will be triggered when
+ # the bot establishes its connection with the server
+ # and the XML streams are ready for use. We want to
+ # listen for this event so that we we can intialize
+ # our roster.
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ """
+ Process the session_start event.
+
+ Typical actions for the session_start event are
+ requesting the roster and broadcasting an intial
+ presence stanza.
+
+ In this case, we send disco#info and disco#items
+ stanzas to the requested JID and print the results.
+
+ Arguments:
+ event -- An empty dictionary. The session_start
+ event does not provide any additional
+ data.
+ """
+ self.get_roster()
+ self.send_presence()
+
+ if self.get in self.info_types:
+ # By using block=True, the result stanza will be
+ # returned. Execution will block until the reply is
+ # received. Non-blocking options would be to listen
+ # for the disco_info event, or passing a handler
+ # function using the callback parameter.
+ info = self['xep_0030'].get_info(jid=self.target_jid,
+ node=self.target_node,
+ block=True)
+ if self.get in self.items_types:
+ # The same applies from above. Listen for the
+ # disco_items event or pass a callback function
+ # if you need to process a non-blocking request.
+ items = self['xep_0030'].get_items(jid=self.target_jid,
+ node=self.target_node,
+ block=True)
+ else:
+ logging.error("Invalid disco request type.")
+ self.disconnect()
+ return
+
+ header = 'XMPP Service Discovery: %s' % self.target_jid
+ print(header)
+ print('-' * len(header))
+ if self.target_node != '':
+ print('Node: %s' % self.target_node)
+ print('-' * len(header))
+
+ if self.get in self.identity_types:
+ print('Identities:')
+ for identity in info['disco_info']['identities']:
+ print(' - ', identity)
+
+ if self.get in self.feature_types:
+ print('Features:')
+ for feature in info['disco_info']['features']:
+ print(' - %s' % feature)
+
+ if self.get in self.items_types:
+ print('Items:')
+ for item in items['disco_items']['items']:
+ print(' - %s' % str(item))
+
+ self.disconnect()
+
+
+if __name__ == '__main__':
+ # Setup the command line arguments.
+ optp = OptionParser()
+ optp.version = '%%prog 0.1'
+ optp.usage = "Usage: %%prog [options] %s <jid> [<node>]" % \
+ 'all|info|items|identities|features'
+
+ optp.add_option('-q','--quiet', help='set logging to ERROR',
+ action='store_const',
+ dest='loglevel',
+ const=logging.ERROR,
+ default=logging.ERROR)
+ optp.add_option('-d','--debug', help='set logging to DEBUG',
+ action='store_const',
+ dest='loglevel',
+ const=logging.DEBUG,
+ default=logging.ERROR)
+ optp.add_option('-v','--verbose', help='set logging to COMM',
+ action='store_const',
+ dest='loglevel',
+ const=5,
+ default=logging.ERROR)
+
+ # JID and password options.
+ optp.add_option("-j", "--jid", dest="jid",
+ help="JID to use")
+ optp.add_option("-p", "--password", dest="password",
+ help="password to use")
+ opts,args = optp.parse_args()
+
+ # Setup logging.
+ logging.basicConfig(level=opts.loglevel,
+ format='%(levelname)-8s %(message)s')
+
+ if len(args) < 2:
+ optp.print_help()
+ exit()
+
+ if len(args) == 2:
+ args = (args[0], args[1], '')
+
+ if opts.jid is None:
+ opts.jid = raw_input("Username: ")
+ if opts.password is None:
+ opts.password = getpass.getpass("Password: ")
+
+ # Setup the Disco browser.
+ xmpp = Disco(opts.jid, opts.password, args[1], args[2], args[0])
+
+ # If you are working with an OpenFire server, you may need
+ # to adjust the SSL version used:
+ # xmpp.ssl_version = ssl.PROTOCOL_SSLv3
+
+ # If you want to verify the SSL certificates offered by a server:
+ # xmpp.ca_certs = "path/to/ca/cert"
+
+ # Connect to the XMPP server and start processing XMPP stanzas.
+ if xmpp.connect():
+ # If you do not have the pydns library installed, you will need
+ # to manually specify the name of the server if it does not match
+ # the one in the JID. For example, to use Google Talk you would
+ # need to use:
+ #
+ # if xmpp.connect(('talk.google.com', 5222)):
+ # ...
+ xmpp.process(threaded=False)
+ else:
+ print("Unable to connect.")
diff --git a/examples/echo_client.py b/examples/echo_client.py
index f449ce4e..099f8eea 100755
--- a/examples/echo_client.py
+++ b/examples/echo_client.py
@@ -118,6 +118,13 @@ if __name__ == '__main__':
xmpp.registerPlugin('xep_0060') # PubSub
xmpp.registerPlugin('xep_0199') # XMPP Ping
+ # If you are working with an OpenFire server, you may need
+ # to adjust the SSL version used:
+ # xmpp.ssl_version = ssl.PROTOCOL_SSLv3
+
+ # If you want to verify the SSL certificates offered by a server:
+ # xmpp.ca_certs = "path/to/ca/cert"
+
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
diff --git a/setup.py b/setup.py
index cf94b4d7..93c9ab33 100644
--- a/setup.py
+++ b/setup.py
@@ -38,13 +38,15 @@ CLASSIFIERS = [ 'Intended Audience :: Developers',
]
packages = [ 'sleekxmpp',
- 'sleekxmpp/plugins',
'sleekxmpp/stanza',
'sleekxmpp/test',
'sleekxmpp/xmlstream',
'sleekxmpp/xmlstream/matcher',
'sleekxmpp/xmlstream/handler',
'sleekxmpp/thirdparty',
+ 'sleekxmpp/plugins',
+ 'sleekxmpp/plugins/xep_0030',
+ 'sleekxmpp/plugins/xep_0030/stanza'
]
if sys.version_info < (3, 0):
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
index 5a232d14..bc02e5f6 100644
--- a/sleekxmpp/basexmpp.py
+++ b/sleekxmpp/basexmpp.py
@@ -271,7 +271,7 @@ class BaseXMPP(XMLStream):
"""Create a Presence stanza associated with this stream."""
return Presence(self, *args, **kwargs)
- def make_iq(self, id=0, ifrom=None):
+ def make_iq(self, id=0, ifrom=None, ito=None, type=None, query=None):
"""
Create a new Iq stanza with a given Id and from JID.
@@ -279,11 +279,19 @@ class BaseXMPP(XMLStream):
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})
+ ito -- The destination JID for this stanza.
+ type -- The Iq's type, one of: get, set, result, or error.
+ query -- Optional namespace for adding a query element.
+ """
+ iq = self.Iq()
+ iq['id'] = str(id)
+ iq['to'] = ito
+ iq['from'] = ifrom
+ iq['type'] = itype
+ iq['query'] = query
+ return iq
- def make_iq_get(self, queryxmlns=None):
+ def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None):
"""
Create an Iq stanza of type 'get'.
@@ -291,21 +299,45 @@ class BaseXMPP(XMLStream):
Arguments:
queryxmlns -- The namespace of the query to use.
+ ito -- The destination JID for this stanza.
+ ifrom -- The from JID to use for this stanza.
+ iq -- Optionally use an existing stanza instead
+ of generating a new one.
"""
- return self.Iq()._set_stanza_values({'type': 'get',
- 'query': queryxmlns})
+ if not iq:
+ iq = self.Iq()
+ iq['type'] = 'get'
+ iq['query'] = queryxmlns
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
- def make_iq_result(self, id):
+ def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None):
"""
Create an Iq stanza of type 'result' with the given ID value.
Arguments:
- id -- An ideally unique ID value. May use self.new_id().
+ id -- An ideally unique ID value. May use self.new_id().
+ ito -- The destination JID for this stanza.
+ ifrom -- The from JID to use for this stanza.
+ iq -- Optionally use an existing stanza instead
+ of generating a new one.
"""
- return self.Iq()._set_stanza_values({'id': id,
- 'type': 'result'})
+ if not iq:
+ iq = self.Iq()
+ if id is None:
+ id = self.new_id()
+ iq['id'] = id
+ iq['type'] = 'result'
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
- def make_iq_set(self, sub=None):
+ def make_iq_set(self, sub=None, ito=None, ifrom=None, iq=None):
"""
Create an Iq stanza of type 'set'.
@@ -313,15 +345,26 @@ class BaseXMPP(XMLStream):
stanza's payload.
Arguments:
- sub -- A stanza or XML object to use as the Iq's payload.
+ sub -- A stanza or XML object to use as the Iq's payload.
+ ito -- The destination JID for this stanza.
+ ifrom -- The from JID to use for this stanza.
+ iq -- Optionally use an existing stanza instead
+ of generating a new one.
"""
- iq = self.Iq()._set_stanza_values({'type': 'set'})
+ if not iq:
+ iq = self.Iq()
+ iq['type'] = 'set'
if sub != None:
iq.append(sub)
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
return iq
def make_iq_error(self, id, type='cancel',
- condition='feature-not-implemented', text=None):
+ condition='feature-not-implemented',
+ text=None, ito=None, ifrom=None, iq=None):
"""
Create an Iq stanza of type 'error'.
@@ -332,14 +375,24 @@ class BaseXMPP(XMLStream):
condition -- The error condition.
Defaults to 'feature-not-implemented'.
text -- A message describing the cause of the error.
+ ito -- The destination JID for this stanza.
+ ifrom -- The from JID to use for this stanza.
+ iq -- Optionally use an existing stanza instead
+ of generating a new one.
"""
- iq = self.Iq()._set_stanza_values({'id': id})
- iq['error']._set_stanza_values({'type': type,
- 'condition': condition,
- 'text': text})
+ if not iq:
+ iq = self.Iq()
+ iq['id'] = id
+ iq['error']['type'] = type
+ iq['error']['condition'] = condition
+ iq['error']['text'] = text
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
return iq
- def make_iq_query(self, iq=None, xmlns=''):
+ def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None):
"""
Create or modify an Iq stanza to use the given
query namespace.
@@ -348,10 +401,16 @@ class BaseXMPP(XMLStream):
iq -- Optional Iq stanza to modify. A new
stanza is created otherwise.
xmlns -- The query's namespace.
+ ito -- The destination JID for this stanza.
+ ifrom -- The from JID to use for this stanza.
"""
if not iq:
iq = self.Iq()
iq['query'] = xmlns
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
return iq
def make_query_roster(self, iq=None):
diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py
index 427ab04e..d27937ae 100644
--- a/sleekxmpp/plugins/__init__.py
+++ b/sleekxmpp/plugins/__init__.py
@@ -5,6 +5,6 @@
See the file LICENSE for copying permission.
"""
-__all__ = ['xep_0004', 'xep_0012', 'xep_0030', 'xep_0033', 'xep_0045',
- 'xep_0050', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify',
- 'xep_0060', 'xep_0202']
+__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
+ 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086',
+ 'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify']
diff --git a/sleekxmpp/plugins/gmail_notify.py b/sleekxmpp/plugins/gmail_notify.py
index 7e888b90..9a94a413 100644
--- a/sleekxmpp/plugins/gmail_notify.py
+++ b/sleekxmpp/plugins/gmail_notify.py
@@ -143,7 +143,7 @@ class gmail_notify(base.base_plugin):
log.info('Gmail: Searching for emails matching: "%s"' % query)
iq = self.xmpp.Iq()
iq['type'] = 'get'
- iq['to'] = self.xmpp.jid
+ iq['to'] = self.xmpp.boundjid.bare
iq['gmail']['q'] = query
iq['gmail']['newer-than-time'] = newer
return iq.send()
diff --git a/sleekxmpp/plugins/jobs.py b/sleekxmpp/plugins/jobs.py
index 0b93d62e..0f1f7fb1 100644
--- a/sleekxmpp/plugins/jobs.py
+++ b/sleekxmpp/plugins/jobs.py
@@ -1,7 +1,6 @@
from . import base
import logging
from xml.etree import cElementTree as ET
-import types
log = logging.getLogger(__name__)
@@ -43,7 +42,7 @@ class jobs(base.base_plugin):
iq['psstate']['item'] = jobid
iq['psstate']['payload'] = state
result = iq.send()
- if result is None or type(result) == types.BooleanType or result['type'] != 'result':
+ if result is None or type(result) == bool or result['type'] != 'result':
log.error("Unable to change %s:%s to %s" % (node, jobid, state))
return False
return True
diff --git a/sleekxmpp/plugins/xep_0004.py b/sleekxmpp/plugins/xep_0004.py
index b8b7ebfa..5d41d269 100644
--- a/sleekxmpp/plugins/xep_0004.py
+++ b/sleekxmpp/plugins/xep_0004.py
@@ -13,7 +13,6 @@ from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.message import Message
-import types
log = logging.getLogger(__name__)
@@ -203,7 +202,7 @@ class Form(ElementBase):
def merge(self, other):
new = copy.copy(self)
- if type(other) == types.DictType:
+ if type(other) == dict:
new.setValues(other)
return new
nfields = new.getFields(use_dict=True)
diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py
index c323ba7c..ad3d0ae2 100644
--- a/sleekxmpp/plugins/xep_0030/disco.py
+++ b/sleekxmpp/plugins/xep_0030/disco.py
@@ -26,30 +26,66 @@ class xep_0030(base_plugin):
"""
XEP-0030: Service Discovery
+ Service discovery in XMPP allows entities to discover information about
+ other agents in the network, such as the feature sets supported by a
+ client, or signposts to other, related entities.
+
+ Also see <http://www.xmpp.org/extensions/xep-0030.html>.
+
+ The XEP-0030 plugin works using a hierarchy of dynamic
+ node handlers, ranging from global handlers to specific
+ JID+node handlers. The default set of handlers operate
+ in a static manner, storing disco information in memory.
+ However, custom handlers may use any available backend
+ storage mechanism desired, such as SQLite or Redis.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
Stream Handlers:
- Disco Info --
- Disco Items --
+ Disco Info -- Any Iq stanze that includes a query with the
+ namespace http://jabber.org/protocol/disco#info.
+ Disco Items -- Any Iq stanze that includes a query with the
+ namespace http://jabber.org/protocol/disco#items.
Events:
- disco_info --
- disco_items --
- disco_info_query --
- disco_items_query --
+ disco_info -- Received a disco#info Iq query result.
+ disco_items -- Received a disco#items Iq query result.
+ disco_info_query -- Received a disco#info Iq query request.
+ disco_items_query -- Received a disco#items Iq query request.
+
+ Attributes:
+ stanza -- A reference to the module containing the stanza classes
+ provided by this plugin.
+ static -- Object containing the default set of static node handlers.
+ xmpp -- The main SleekXMPP object.
Methods:
- set_node_handler --
- del_node_handler --
- add_identity --
+ set_node_handler -- Assign a handler to a JID/node combination.
+ del_node_handler -- Remove a handler from a JID/node combination.
+ get_info -- Retrieve disco#info data, locally or remote.
+ get_items -- Retrieve disco#items data, locally or remote.
+ set_identities --
+ set_features --
+ set_items --
+ del_items --
del_identity --
- add_feature --
del_feature --
- add_item --
del_item --
- get_info --
- get_items --
+ add_identity --
+ add_feature --
+ add_item --
"""
def plugin_init(self):
+ """
+ Start the XEP-0030 plugin.
+ """
self.xep = '0030'
self.description = 'Service Discovery'
self.stanza = sleekxmpp.plugins.xep_0030.stanza
@@ -70,42 +106,89 @@ class xep_0030(base_plugin):
self.static = StaticDisco(self.xmpp)
self._disco_ops = ['get_info', 'set_identities', 'set_features',
- 'del_info', 'get_items', 'set_items', 'del_items',
+ 'get_items', 'set_items', 'del_items',
'add_identity', 'del_identity', 'add_feature',
'del_feature', 'add_item', 'del_item']
- self.handlers = {}
+ self._handlers = {}
for op in self._disco_ops:
- self.handlers[op] = {'global': getattr(self.static, op),
- 'jid': {},
- 'node': {}}
-
+ self._handlers[op] = {'global': getattr(self.static, op),
+ 'jid': {},
+ 'node': {}}
def set_node_handler(self, htype, jid=None, node=None, handler=None):
"""
+ Add a node handler for the given hierarchy level and
+ handler type.
+
+ Node handlers are ordered in a hierarchy where the
+ most specific handler is executed. Thus, a fallback,
+ global handler can be used for the majority of cases
+ with a few node specific handler that override the
+ global behavior.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
+ Handler types:
+ get_info
+ get_items
+ set_identities
+ set_features
+ set_items
+ del_items
+ del_identity
+ del_feature
+ del_item
+ add_identity
+ add_feature
+ add_item
+
Arguments:
- htype
- jid
- node
- handler
+ htype -- The operation provided by the handler.
+ jid -- The JID the handler applies to. May be narrowed
+ further if a node is given.
+ node -- The particular node the handler is for. If no JID
+ is given, then the self.xmpp.boundjid.full is
+ assumed.
+ handler -- The handler function to use.
"""
if htype not in self._disco_ops:
return
if jid is None and node is None:
- self.handlers[htype]['global'] = handler
+ self._handlers[htype]['global'] = handler
elif node is None:
- self.handlers[htype]['jid'][jid] = handler
+ self._handlers[htype]['jid'][jid] = handler
elif jid is None:
jid = self.xmpp.boundjid.full
- self.handlers[htype]['node'][(jid, node)] = handler
+ self._handlers[htype]['node'][(jid, node)] = handler
else:
- self.handlers[htype]['node'][(jid, node)] = handler
+ self._handlers[htype]['node'][(jid, node)] = handler
def del_node_handler(self, htype, jid, node):
"""
+ Remove a handler type for a JID and node combination.
+
+ The next handler in the hierarchy will be used if one
+ exists. If removing the global handler, make sure that
+ other handlers exist to process existing nodes.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
Arguments:
- htype
- jid
- node
+ htype -- The type of handler to remove.
+ jid -- The JID from which to remove the handler.
+ node -- The node from which to remove the handler.
"""
self.set_node_handler(htype, jid, node, None)
@@ -132,14 +215,28 @@ class xep_0030(base_plugin):
def get_info(self, jid=None, node=None, local=False, **kwargs):
"""
+ Retrieve the disco#info results from a given JID/node combination.
+
+ Info may be retrieved from both local resources and remote agents;
+ the local parameter indicates if the information should be gathered
+ by executing the local node handlers, or if a disco#info stanza
+ must be generated and sent.
+
Arguments:
- jid --
- node --
- local --
- dfrom --
- block --
- timeout --
- callback --
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ dfrom -- Specifiy the sender's JID.
+ block -- If true, block and wait for the stanzas' reply.
+ timeout -- The time in seconds to block while waiting for
+ a reply. If None, then wait indefinitely.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
"""
if local or jid is None:
log.debug("Looking up local disco#info data " + \
@@ -158,14 +255,28 @@ class xep_0030(base_plugin):
def get_items(self, jid=None, node=None, local=False, **kwargs):
"""
+ Retrieve the disco#items results from a given JID/node combination.
+
+ Items may be retrieved from both local resources and remote agents;
+ the local parameter indicates if the items should be gathered by
+ executing the local node handlers, or if a disco#items stanza must
+ be generated and sent.
+
Arguments:
- jid --
- node --
- local --
- dfrom --
- block --
- timeout --
- callback --
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the items.
+ dfrom -- Specifiy the sender's JID.
+ block -- If true, block and wait for the stanzas' reply.
+ timeout -- The time in seconds to block while waiting for
+ a reply. If None, then wait indefinitely.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
"""
if local or jid is None:
return self._run_node_handler('get_items', jid, node, kwargs)
@@ -179,37 +290,169 @@ class xep_0030(base_plugin):
block=kwargs.get('block', None),
callback=kwargs.get('callback', None))
- def set_info(self, jid=None, node=None, **kwargs):
- self._run_node_handler('set_info', jid, node, kwargs)
+ def set_items(self, jid=None, node=None, **kwargs):
+ """
+ Set or replace all items for the specified JID/node combination.
- def del_info(self, jid=None, node=None, **kwargs):
- self._run_node_handler('del_info', jid, node, kwargs)
+ The given items must be in a list or set where each item is a
+ tuple of the form: (jid, node, name).
- def set_items(self, jid=None, node=None, **kwargs):
+ Arguments:
+ jid -- The JID to modify.
+ node -- Optional node to modify.
+ items -- A series of items in tuple format.
+ """
self._run_node_handler('set_items', jid, node, kwargs)
def del_items(self, jid=None, node=None, **kwargs):
+ """
+ Remove all items from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- Optional node to modify.
+ """
self._run_node_handler('del_items', jid, node, kwargs)
+ def add_item(self, jid=None, node=None, **kwargs):
+ """
+ Add a new item element to the given JID/node combination.
+
+ Each item is required to have a JID, but may also specify
+ a node value to reference non-addressable entities.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ ijid -- The JID for the item.
+ inode -- Optional node for the item.
+ name -- Optional name for the item.
+ """
+ self._run_node_handler('add_item', jid, node, kwargs)
+
+ def del_item(self, jid=None, node=None, **kwargs):
+ """
+ Remove a single item from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ ijid -- The item's JID.
+ inode -- The item's node.
+ """
+ self._run_node_handler('del_item', jid, node, kwargs)
+
def add_identity(self, jid=None, node=None, **kwargs):
+ """
+ Add a new identity to the given JID/node combination.
+
+ Each identity must be unique in terms of all four identity
+ components: category, type, name, and language.
+
+ Multiple, identical category/type pairs are allowed only
+ if the xml:lang values are different. Likewise, multiple
+ category/type/xml:lang pairs are allowed so long as the
+ names are different. A category and type is always required.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ category -- The identity's category.
+ itype -- The identity's type.
+ name -- Optional name for the identity.
+ lang -- Optional two-letter language code.
+ """
self._run_node_handler('add_identity', jid, node, kwargs)
def add_feature(self, jid=None, node=None, **kwargs):
+ """
+ Add a feature to a JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ feature -- The namespace of the supported feature.
+ """
self._run_node_handler('add_feature', jid, node, kwargs)
def del_identity(self, jid=None, node=None, **kwargs):
+ """
+ Remove an identity from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ category -- The identity's category.
+ itype -- The identity's type value.
+ name -- Optional, human readable name for the identity.
+ lang -- Optional, the identity's xml:lang value.
+ """
self._run_node_handler('del_identity', jid, node, kwargs)
def del_feature(self, jid=None, node=None, **kwargs):
+ """
+ Remove a feature from a given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ feature -- The feature's namespace.
+ """
self._run_node_handler('del_feature', jid, node, kwargs)
- def add_item(self, jid=None, node=None, **kwargs):
- self._run_node_handler('add_item', jid, node, kwargs)
+ def set_identities(self, jid=None, node=None, **kwargs):
+ """
+ Add or replace all identities for the given JID/node combination.
- def del_item(self, jid=None, node=None, **kwargs):
- self._run_node_handler('del_item', jid, node, kwargs)
+ The identities must be in a set where each identity is a tuple
+ of the form: (category, type, lang, name)
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ identities -- A set of identities in tuple form.
+ lang -- Optional, xml:lang value.
+ """
+ self._run_node_handler('set_identities', jid, node, kwargs)
- def _run_node_handler(self, htype, jid, node, data=None):
+ def del_identities(self, jid=None, node=None, **kwargs):
+ """
+ Remove all identities for a JID/node combination.
+
+ If a language is specified, only identities using that
+ language will be removed.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ lang -- Optional. If given, only remove identities
+ using this xml:lang value.
+ """
+ self._run_node_handler('del_identities', jid, node, kwargs)
+
+ def set_features(self, jid=None, node=None, **kwargs):
+ """
+ Add or replace the set of supported features
+ for a JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ features -- The new set of supported features.
+ """
+ self._run_node_handler('set_features', jid, node, kwargs)
+
+ def del_features(self, jid=None, node=None, **kwargs):
+ """
+ Remove all features from a JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ """
+ self._run_node_handler('del_features', jid, node, kwargs)
+
+ def _run_node_handler(self, htype, jid, node, data={}):
"""
Execute the most specific node handler for the given
JID/node combination.
@@ -218,19 +461,19 @@ class xep_0030(base_plugin):
htype -- The handler type to execute.
jid -- The JID requested.
node -- The node requested.
- dat -- Optional, custom data to pass to the handler.
+ data -- Optional, custom data to pass to the handler.
"""
if jid is None:
jid = self.xmpp.boundjid.full
if node is None:
node = ''
- if self.handlers[htype]['node'].get((jid, node), False):
- return self.handlers[htype]['node'][(jid, node)](jid, node, data)
- elif self.handlers[htype]['jid'].get(jid, False):
- return self.handlers[htype]['jid'][jid](jid, node, data)
- elif self.handlers[htype]['global']:
- return self.handlers[htype]['global'](jid, node, data)
+ if self._handlers[htype]['node'].get((jid, node), False):
+ return self._handlers[htype]['node'][(jid, node)](jid, node, data)
+ elif self._handlers[htype]['jid'].get(jid, False):
+ return self._handlers[htype]['jid'][jid](jid, node, data)
+ elif self._handlers[htype]['global']:
+ return self._handlers[htype]['global'](jid, node, data)
else:
return None
@@ -311,4 +554,3 @@ class xep_0030(base_plugin):
"Using default disco#info feature.")
info.add_feature(info.namespace)
return info
-
diff --git a/sleekxmpp/plugins/xep_0030/stanza/disco.py b/sleekxmpp/plugins/xep_0030/stanza/disco.py
deleted file mode 100644
index e69de29b..00000000
--- a/sleekxmpp/plugins/xep_0030/stanza/disco.py
+++ /dev/null
diff --git a/sleekxmpp/plugins/xep_0030/stanza/items.py b/sleekxmpp/plugins/xep_0030/stanza/items.py
index 319e666f..a1fb819c 100644
--- a/sleekxmpp/plugins/xep_0030/stanza/items.py
+++ b/sleekxmpp/plugins/xep_0030/stanza/items.py
@@ -12,8 +12,6 @@ from sleekxmpp.xmlstream import ElementBase, ET
class DiscoItems(ElementBase):
"""
-
-
Example disco#items stanzas:
<iq type="get">
<query xmlns="http://jabber.org/protocol/disco#items" />
@@ -74,7 +72,7 @@ class DiscoItems(ElementBase):
Arguments:
jid -- The JID for the item.
- node -- Optional additional information to reference
+ node -- Optional additional information to reference
non-addressable items.
name -- Optional human readable name for the item.
"""
diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py
index f3693228..b0e931b4 100644
--- a/sleekxmpp/plugins/xep_0030/static.py
+++ b/sleekxmpp/plugins/xep_0030/static.py
@@ -35,6 +35,11 @@ class StaticDisco(object):
def __init__(self, xmpp):
"""
+ Create a static disco interface. Sets of disco#info and
+ disco#items are maintained for every given JID and node
+ combination. These stanzas are used to store disco
+ information in memory without any additional processing.
+
Arguments:
xmpp -- The main SleekXMPP object.
"""
@@ -52,7 +57,7 @@ class StaticDisco(object):
self.nodes[(jid, node)]['info']['node'] = node
self.nodes[(jid, node)]['items']['node'] = node
- def get_info(self, jid, node, data=None):
+ def get_info(self, jid, node, data):
if (jid, node) not in self.nodes:
if not node:
return DiscoInfo()
@@ -61,11 +66,11 @@ class StaticDisco(object):
else:
return self.nodes[(jid, node)]['info']
- def del_info(self, jid, node, data=None):
+ def del_info(self, jid, node, data):
if (jid, node) in self.nodes:
self.nodes[(jid, node)]['info'] = DiscoInfo()
- def get_items(self, jid, node, data=None):
+ def get_items(self, jid, node, data):
if (jid, node) not in self.nodes:
if not node:
return DiscoInfo()
@@ -74,14 +79,16 @@ class StaticDisco(object):
else:
return self.nodes[(jid, node)]['items']
- def set_items(self, jid, node, data=None):
- pass
+ def set_items(self, jid, node, data):
+ items = data.get('items', set())
+ self.add_node(jid, node)
+ self.nodes[(jid, node)]['items']['items'] = items
- def del_items(self, jid, node, data=None):
+ def del_items(self, jid, node, data):
if (jid, node) in self.nodes:
self.nodes[(jid, node)]['items'] = DiscoItems()
- def add_identity(self, jid, node, data={}):
+ def add_identity(self, jid, node, data):
self.add_node(jid, node)
self.nodes[(jid, node)]['info'].add_identity(
data.get('category', ''),
@@ -89,10 +96,12 @@ class StaticDisco(object):
data.get('name', None),
data.get('lang', None))
- def set_identities(self, jid, node, data=None):
- pass
+ def set_identities(self, jid, node, data):
+ identities = data.get('identities', set())
+ self.add_node(jid, node)
+ self.nodes[(jid, node)]['info']['identities'] = identities
- def del_identity(self, jid, node, data=None):
+ def del_identity(self, jid, node, data):
if (jid, node) not in self.nodes:
return
self.nodes[(jid, node)]['info'].del_identity(
@@ -101,27 +110,29 @@ class StaticDisco(object):
data.get('name', None),
data.get('lang', None))
-
- def add_feature(self, jid, node, data=None):
+ def add_feature(self, jid, node, data):
self.add_node(jid, node)
self.nodes[(jid, node)]['info'].add_feature(data.get('feature', ''))
- def set_features(self, jid, node, data=None):
- pass
+ def set_features(self, jid, node, data):
+ features = data.get('features', set())
+ self.add_node(jid, node)
+ self.nodes[(jid, node)]['info']['features'] = features
- def del_feature(self, jid, node, data=None):
+ def del_feature(self, jid, node, data):
if (jid, node) not in self.nodes:
return
self.nodes[(jid, node)]['info'].del_feature(data.get('feature', ''))
- def add_item(self, jid, node, data=None):
+ def add_item(self, jid, node, data):
self.add_node(jid, node)
self.nodes[(jid, node)]['items'].add_item(
data.get('ijid', ''),
node=data.get('inode', None),
name=data.get('name', None))
- def del_item(self, jid, node, data=None):
+ def del_item(self, jid, node, data):
if (jid, node) in self.nodes:
- self.nodes[(jid, node)]['items'].del_item(**data)
-
+ self.nodes[(jid, node)]['items'].del_item(
+ data.get('ijid', ''),
+ node=data.get('inode', None))
diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py
index db41cdb3..feec70db 100644
--- a/sleekxmpp/plugins/xep_0045.py
+++ b/sleekxmpp/plugins/xep_0045.py
@@ -20,325 +20,333 @@ log = logging.getLogger(__name__)
class MUCPresence(ElementBase):
- name = 'x'
- namespace = 'http://jabber.org/protocol/muc#user'
- plugin_attrib = 'muc'
- interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
- affiliations = set(('', ))
- roles = set(('', ))
-
- def getXMLItem(self):
- item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
- if item is None:
- item = ET.Element('{http://jabber.org/protocol/muc#user}item')
- self.xml.append(item)
- return item
-
- def getAffiliation(self):
- #TODO if no affilation, set it to the default and return default
- item = self.getXMLItem()
- return item.get('affiliation', '')
-
- def setAffiliation(self, value):
- item = self.getXMLItem()
- #TODO check for valid affiliation
- item.attrib['affiliation'] = value
- return self
-
- def delAffiliation(self):
- item = self.getXMLItem()
- #TODO set default affiliation
- if 'affiliation' in item.attrib: del item.attrib['affiliation']
- return self
-
- def getJid(self):
- item = self.getXMLItem()
- return JID(item.get('jid', ''))
-
- def setJid(self, value):
- item = self.getXMLItem()
- if not isinstance(value, str):
- value = str(value)
- item.attrib['jid'] = value
- return self
-
- def delJid(self):
- item = self.getXMLItem()
- if 'jid' in item.attrib: del item.attrib['jid']
- return self
-
- def getRole(self):
- item = self.getXMLItem()
- #TODO get default role, set default role if none
- return item.get('role', '')
-
- def setRole(self, value):
- item = self.getXMLItem()
- #TODO check for valid role
- item.attrib['role'] = value
- return self
-
- def delRole(self):
- item = self.getXMLItem()
- #TODO set default role
- if 'role' in item.attrib: del item.attrib['role']
- return self
-
- def getNick(self):
- return self.parent()['from'].resource
-
- def getRoom(self):
- return self.parent()['from'].bare
-
- def setNick(self, value):
- log.warning("Cannot set nick through mucpresence plugin.")
- return self
-
- def setRoom(self, value):
- log.warning("Cannot set room through mucpresence plugin.")
- return self
-
- def delNick(self):
- log.warning("Cannot delete nick through mucpresence plugin.")
- return self
-
- def delRoom(self):
- log.warning("Cannot delete room through mucpresence plugin.")
- return self
+ name = 'x'
+ namespace = 'http://jabber.org/protocol/muc#user'
+ plugin_attrib = 'muc'
+ interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
+ affiliations = set(('', ))
+ roles = set(('', ))
+
+ def getXMLItem(self):
+ item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
+ if item is None:
+ item = ET.Element('{http://jabber.org/protocol/muc#user}item')
+ self.xml.append(item)
+ return item
+
+ def getAffiliation(self):
+ #TODO if no affilation, set it to the default and return default
+ item = self.getXMLItem()
+ return item.get('affiliation', '')
+
+ def setAffiliation(self, value):
+ item = self.getXMLItem()
+ #TODO check for valid affiliation
+ item.attrib['affiliation'] = value
+ return self
+
+ def delAffiliation(self):
+ item = self.getXMLItem()
+ #TODO set default affiliation
+ if 'affiliation' in item.attrib: del item.attrib['affiliation']
+ return self
+
+ def getJid(self):
+ item = self.getXMLItem()
+ return JID(item.get('jid', ''))
+
+ def setJid(self, value):
+ item = self.getXMLItem()
+ if not isinstance(value, str):
+ value = str(value)
+ item.attrib['jid'] = value
+ return self
+
+ def delJid(self):
+ item = self.getXMLItem()
+ if 'jid' in item.attrib: del item.attrib['jid']
+ return self
+
+ def getRole(self):
+ item = self.getXMLItem()
+ #TODO get default role, set default role if none
+ return item.get('role', '')
+
+ def setRole(self, value):
+ item = self.getXMLItem()
+ #TODO check for valid role
+ item.attrib['role'] = value
+ return self
+
+ def delRole(self):
+ item = self.getXMLItem()
+ #TODO set default role
+ if 'role' in item.attrib: del item.attrib['role']
+ return self
+
+ def getNick(self):
+ return self.parent()['from'].resource
+
+ def getRoom(self):
+ return self.parent()['from'].bare
+
+ def setNick(self, value):
+ log.warning("Cannot set nick through mucpresence plugin.")
+ return self
+
+ def setRoom(self, value):
+ log.warning("Cannot set room through mucpresence plugin.")
+ return self
+
+ def delNick(self):
+ log.warning("Cannot delete nick through mucpresence plugin.")
+ return self
+
+ def delRoom(self):
+ log.warning("Cannot delete room through mucpresence plugin.")
+ return self
class xep_0045(base.base_plugin):
- """
- Impliments XEP-0045 Multi User Chat
- """
-
- def plugin_init(self):
- self.rooms = {}
- self.ourNicks = {}
- self.xep = '0045'
- self.description = 'Multi User Chat'
- # load MUC support in presence stanzas
- registerStanzaPlugin(Presence, MUCPresence)
- self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
- self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
- self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
-
- def handle_groupchat_presence(self, pr):
- """ Handle a presence in a muc.
- """
- got_offline = False
- got_online = False
- if pr['muc']['room'] not in self.rooms.keys():
- return
- entry = pr['muc'].getStanzaValues()
- entry['show'] = pr['show']
- entry['status'] = pr['status']
- if pr['type'] == 'unavailable':
- if entry['nick'] in self.rooms[entry['room']]:
- del self.rooms[entry['room']][entry['nick']]
- got_offline = True
- else:
- if entry['nick'] not in self.rooms[entry['room']]:
- got_online = True
- self.rooms[entry['room']][entry['nick']] = entry
- log.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry))
- self.xmpp.event("groupchat_presence", pr)
- self.xmpp.event("muc::%s::presence" % entry['room'], pr)
- if got_offline:
- self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
- if got_online:
- self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
-
- def handle_groupchat_message(self, msg):
- """ Handle a message event in a muc.
- """
- self.xmpp.event('groupchat_message', msg)
- self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
-
- def handle_groupchat_subject(self, msg):
- """ Handle a message coming from a muc indicating
- a change of subject (or announcing it when joining the room)
- """
- self.xmpp.event('groupchat_subject', msg)
-
- def jidInRoom(self, room, jid):
- for nick in self.rooms[room]:
- entry = self.rooms[room][nick]
- if entry is not None and entry['jid'].full == jid:
- return True
- return False
-
- def getNick(self, room, jid):
- for nick in self.rooms[room]:
- entry = self.rooms[room][nick]
- if entry is not None and entry['jid'].full == jid:
- return nick
-
- def getRoomForm(self, room, ifrom=None):
- iq = self.xmpp.makeIqGet()
- iq['to'] = room
- if ifrom is not None:
- iq['from'] = ifrom
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- iq.append(query)
- result = iq.send()
- if result['type'] == 'error':
- return False
- xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
- if xform is None: return False
- form = self.xmpp.plugin['old_0004'].buildForm(xform)
- return form
-
- def configureRoom(self, room, form=None, ifrom=None):
- if form is None:
- form = self.getRoomForm(room, ifrom=ifrom)
- #form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit')
- #form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
- iq = self.xmpp.makeIqSet()
- iq['to'] = room
- if ifrom is not None:
- iq['from'] = ifrom
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- form = form.getXML('submit')
- query.append(form)
- iq.append(query)
- result = iq.send()
- if result['type'] == 'error':
- return False
- return True
-
- def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
- """ Join the specified room, requesting 'maxhistory' lines of history.
- """
- stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow)
- x = ET.Element('{http://jabber.org/protocol/muc}x')
- if password:
- passelement = ET.Element('password')
- passelement.text = password
- x.append(passelement)
- if maxhistory:
- history = ET.Element('history')
- if maxhistory == "0":
- history.attrib['maxchars'] = maxhistory
- else:
- history.attrib['maxstanzas'] = maxhistory
- x.append(history)
- stanza.append(x)
- if not wait:
- self.xmpp.send(stanza)
- else:
- #wait for our own room presence back
- expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
- self.xmpp.send(stanza, expect)
- self.rooms[room] = {}
- self.ourNicks[room] = nick
-
- def destroy(self, room, reason='', altroom = '', ifrom=None):
- iq = self.xmpp.makeIqSet()
- if ifrom is not None:
- iq['from'] = ifrom
- iq['to'] = room
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- destroy = ET.Element('destroy')
- if altroom:
- destroy.attrib['jid'] = altroom
- xreason = ET.Element('reason')
- xreason.text = reason
- destroy.append(xreason)
- query.append(destroy)
- iq.append(query)
- r = iq.send()
- if r is False or r['type'] == 'error':
- return False
- return True
-
- def setAffiliation(self, room, jid=None, nick=None, affiliation='member'):
- """ Change room affiliation."""
- if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
- raise TypeError
- query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
- if nick is not None:
- item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
- else:
- item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
- query.append(item)
- iq = self.xmpp.makeIqSet(query)
- iq['to'] = room
- result = iq.send()
- if result is False or result['type'] != 'result':
- raise ValueError
- return True
-
- def invite(self, room, jid, reason=''):
- """ Invite a jid to a room."""
- msg = self.xmpp.makeMessage(room)
- msg['from'] = self.xmpp.jid
- x = ET.Element('{http://jabber.org/protocol/muc#user}x')
- invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
- if reason:
- rxml = ET.Element('reason')
- rxml.text = reason
- invite.append(rxml)
- x.append(invite)
- msg.append(x)
- self.xmpp.send(msg)
-
- def leaveMUC(self, room, nick, msg=''):
- """ Leave the specified room.
- """
- if msg:
- self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg)
- else:
- self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
- del self.rooms[room]
-
- def getRoomConfig(self, room):
- iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
- iq['to'] = room
- iq['from'] = self.xmpp.jid
- result = iq.send()
- if result is None or result['type'] != 'result':
- raise ValueError
- form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
- if form is None:
- raise ValueError
- return self.xmpp.plugin['xep_0004'].buildForm(form)
-
- def cancelConfig(self, room):
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- x = ET.Element('{jabber:x:data}x', type='cancel')
- query.append(x)
- iq = self.xmpp.makeIqSet(query)
- iq.send()
-
- def setRoomConfig(self, room, config):
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- x = config.getXML('submit')
- query.append(x)
- iq = self.xmpp.makeIqSet(query)
- iq['to'] = room
- iq['from'] = self.xmpp.jid
- iq.send()
-
- def getJoinedRooms(self):
- return self.rooms.keys()
-
- def getOurJidInRoom(self, roomJid):
- """ Return the jid we're using in a room.
- """
- return "%s/%s" % (roomJid, self.ourNicks[roomJid])
-
- def getJidProperty(self, room, nick, jidProperty):
- """ Get the property of a nick in a room, such as its 'jid' or 'affiliation'
- If not found, return None.
- """
- if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
- return self.rooms[room][nick][jidProperty]
- else:
- return None
-
- def getRoster(self, room):
- """ Get the list of nicks in a room.
- """
- if room not in self.rooms.keys():
- return None
- return self.rooms[room].keys()
+ """
+ Implements XEP-0045 Multi User Chat
+ """
+
+ def plugin_init(self):
+ self.rooms = {}
+ self.ourNicks = {}
+ self.xep = '0045'
+ self.description = 'Multi User Chat'
+ # load MUC support in presence stanzas
+ registerStanzaPlugin(Presence, MUCPresence)
+ self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
+ self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
+ self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
+ self.xmpp.registerHandler(Callback('MUCInvite', MatchXPath("{%s}message/{http://jabber.org/protocol/muc#user}x/invite" % self.xmpp.default_ns), self.handle_groupchat_invite))
+
+ def handle_groupchat_invite(self, inv):
+ """ Handle an invite into a muc.
+ """
+ logging.debug("MUC invite to %s from %s: %s" % (inv['from'], inv["from"], inv))
+ if inv['from'] not in self.rooms.keys():
+ self.xmpp.event("groupchat_invite", inv)
+
+ def handle_groupchat_presence(self, pr):
+ """ Handle a presence in a muc.
+ """
+ got_offline = False
+ got_online = False
+ if pr['muc']['room'] not in self.rooms.keys():
+ return
+ entry = pr['muc'].getStanzaValues()
+ entry['show'] = pr['show']
+ entry['status'] = pr['status']
+ if pr['type'] == 'unavailable':
+ if entry['nick'] in self.rooms[entry['room']]:
+ del self.rooms[entry['room']][entry['nick']]
+ got_offline = True
+ else:
+ if entry['nick'] not in self.rooms[entry['room']]:
+ got_online = True
+ self.rooms[entry['room']][entry['nick']] = entry
+ log.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry))
+ self.xmpp.event("groupchat_presence", pr)
+ self.xmpp.event("muc::%s::presence" % entry['room'], pr)
+ if got_offline:
+ self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
+ if got_online:
+ self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
+
+ def handle_groupchat_message(self, msg):
+ """ Handle a message event in a muc.
+ """
+ self.xmpp.event('groupchat_message', msg)
+ self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
+
+ def handle_groupchat_subject(self, msg):
+ """ Handle a message coming from a muc indicating
+ a change of subject (or announcing it when joining the room)
+ """
+ self.xmpp.event('groupchat_subject', msg)
+
+ def jidInRoom(self, room, jid):
+ for nick in self.rooms[room]:
+ entry = self.rooms[room][nick]
+ if entry is not None and entry['jid'].full == jid:
+ return True
+ return False
+
+ def getNick(self, room, jid):
+ for nick in self.rooms[room]:
+ entry = self.rooms[room][nick]
+ if entry is not None and entry['jid'].full == jid:
+ return nick
+
+ def getRoomForm(self, room, ifrom=None):
+ iq = self.xmpp.makeIqGet()
+ iq['to'] = room
+ if ifrom is not None:
+ iq['from'] = ifrom
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ iq.append(query)
+ result = iq.send()
+ if result['type'] == 'error':
+ return False
+ xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
+ if xform is None: return False
+ form = self.xmpp.plugin['old_0004'].buildForm(xform)
+ return form
+
+ def configureRoom(self, room, form=None, ifrom=None):
+ if form is None:
+ form = self.getRoomForm(room, ifrom=ifrom)
+ #form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit')
+ #form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
+ iq = self.xmpp.makeIqSet()
+ iq['to'] = room
+ if ifrom is not None:
+ iq['from'] = ifrom
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ form = form.getXML('submit')
+ query.append(form)
+ iq.append(query)
+ result = iq.send()
+ if result['type'] == 'error':
+ return False
+ return True
+
+ def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
+ """ Join the specified room, requesting 'maxhistory' lines of history.
+ """
+ stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow)
+ x = ET.Element('{http://jabber.org/protocol/muc}x')
+ if password:
+ passelement = ET.Element('password')
+ passelement.text = password
+ x.append(passelement)
+ if maxhistory:
+ history = ET.Element('history')
+ if maxhistory == "0":
+ history.attrib['maxchars'] = maxhistory
+ else:
+ history.attrib['maxstanzas'] = maxhistory
+ x.append(history)
+ stanza.append(x)
+ if not wait:
+ self.xmpp.send(stanza)
+ else:
+ #wait for our own room presence back
+ expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
+ self.xmpp.send(stanza, expect)
+ self.rooms[room] = {}
+ self.ourNicks[room] = nick
+
+ def destroy(self, room, reason='', altroom = '', ifrom=None):
+ iq = self.xmpp.makeIqSet()
+ if ifrom is not None:
+ iq['from'] = ifrom
+ iq['to'] = room
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ destroy = ET.Element('destroy')
+ if altroom:
+ destroy.attrib['jid'] = altroom
+ xreason = ET.Element('reason')
+ xreason.text = reason
+ destroy.append(xreason)
+ query.append(destroy)
+ iq.append(query)
+ r = iq.send()
+ if r is False or r['type'] == 'error':
+ return False
+ return True
+
+ def setAffiliation(self, room, jid=None, nick=None, affiliation='member'):
+ """ Change room affiliation."""
+ if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
+ raise TypeError
+ query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
+ if nick is not None:
+ item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
+ else:
+ item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
+ query.append(item)
+ iq = self.xmpp.makeIqSet(query)
+ iq['to'] = room
+ result = iq.send()
+ if result is False or result['type'] != 'result':
+ raise ValueError
+ return True
+
+ def invite(self, room, jid, reason='', mfrom=''):
+ """ Invite a jid to a room."""
+ msg = self.xmpp.makeMessage(room)
+ msg['from'] = mfrom
+ x = ET.Element('{http://jabber.org/protocol/muc#user}x')
+ invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
+ if reason:
+ rxml = ET.Element('reason')
+ rxml.text = reason
+ invite.append(rxml)
+ x.append(invite)
+ msg.append(x)
+ self.xmpp.send(msg)
+
+ def leaveMUC(self, room, nick, msg=''):
+ """ Leave the specified room.
+ """
+ if msg:
+ self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg)
+ else:
+ self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
+ del self.rooms[room]
+
+ def getRoomConfig(self, room, ifrom=''):
+ iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
+ iq['to'] = room
+ iq['from'] = ifrom
+ result = iq.send()
+ if result is None or result['type'] != 'result':
+ raise ValueError
+ form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
+ if form is None:
+ raise ValueError
+ return self.xmpp.plugin['xep_0004'].buildForm(form)
+
+ def cancelConfig(self, room):
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ x = ET.Element('{jabber:x:data}x', type='cancel')
+ query.append(x)
+ iq = self.xmpp.makeIqSet(query)
+ iq.send()
+
+ def setRoomConfig(self, room, config, ifrom=''):
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ x = config.getXML('submit')
+ query.append(x)
+ iq = self.xmpp.makeIqSet(query)
+ iq['to'] = room
+ iq['from'] = ifrom
+ iq.send()
+
+ def getJoinedRooms(self):
+ return self.rooms.keys()
+
+ def getOurJidInRoom(self, roomJid):
+ """ Return the jid we're using in a room.
+ """
+ return "%s/%s" % (roomJid, self.ourNicks[roomJid])
+
+ def getJidProperty(self, room, nick, jidProperty):
+ """ Get the property of a nick in a room, such as its 'jid' or 'affiliation'
+ If not found, return None.
+ """
+ if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
+ return self.rooms[room][nick][jidProperty]
+ else:
+ return None
+
+ def getRoster(self, room):
+ """ Get the list of nicks in a room.
+ """
+ if room not in self.rooms.keys():
+ return None
+ return self.rooms[room].keys()
diff --git a/sleekxmpp/plugins/xep_0050.py b/sleekxmpp/plugins/xep_0050.py
index 5efb9116..439bebb9 100644
--- a/sleekxmpp/plugins/xep_0050.py
+++ b/sleekxmpp/plugins/xep_0050.py
@@ -110,7 +110,7 @@ class xep_0050(base.base_plugin):
if not id:
id = self.xmpp.getNewId()
iq = self.xmpp.makeIqResult(id)
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
iq.attrib['to'] = to
command = ET.Element('{http://jabber.org/protocol/commands}command')
command.attrib['node'] = node
diff --git a/sleekxmpp/plugins/xep_0060.py b/sleekxmpp/plugins/xep_0060.py
index a7c6d023..93124fca 100644
--- a/sleekxmpp/plugins/xep_0060.py
+++ b/sleekxmpp/plugins/xep_0060.py
@@ -51,7 +51,7 @@ class xep_0060(base.base_plugin):
pubsub.append(configure)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is False or result is None or result['type'] == 'error': return False
@@ -63,15 +63,15 @@ class xep_0060(base.base_plugin):
subscribe.attrib['node'] = node
if subscribee is None:
if bare:
- subscribe.attrib['jid'] = self.xmpp.jid
+ subscribe.attrib['jid'] = self.xmpp.boundjid.bare
else:
- subscribe.attrib['jid'] = self.xmpp.fulljid
+ subscribe.attrib['jid'] = self.xmpp.boundjid.full
else:
subscribe.attrib['jid'] = subscribee
pubsub.append(subscribe)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is False or result is None or result['type'] == 'error': return False
@@ -83,15 +83,15 @@ class xep_0060(base.base_plugin):
unsubscribe.attrib['node'] = node
if subscribee is None:
if bare:
- unsubscribe.attrib['jid'] = self.xmpp.jid
+ unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare
else:
- unsubscribe.attrib['jid'] = self.xmpp.fulljid
+ unsubscribe.attrib['jid'] = self.xmpp.boundjid.full
else:
unsubscribe.attrib['jid'] = subscribee
pubsub.append(unsubscribe)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is False or result is None or result['type'] == 'error': return False
@@ -109,7 +109,7 @@ class xep_0060(base.base_plugin):
iq = self.xmpp.makeIqGet()
iq.append(pubsub)
iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
#self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
result = iq.send()
@@ -133,7 +133,7 @@ class xep_0060(base.base_plugin):
iq = self.xmpp.makeIqGet()
iq.append(pubsub)
iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result == False or result['type'] == 'error':
@@ -156,7 +156,7 @@ class xep_0060(base.base_plugin):
iq = self.xmpp.makeIqGet()
iq.append(pubsub)
iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result == False or result['type'] == 'error':
@@ -179,7 +179,7 @@ class xep_0060(base.base_plugin):
pubsub.append(delete)
iq.append(pubsub)
iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
result = iq.send()
if result is not None and result is not False and result['type'] != 'error':
return True
@@ -196,7 +196,7 @@ class xep_0060(base.base_plugin):
pubsub.append(configure)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result['type'] == 'error':
@@ -217,7 +217,7 @@ class xep_0060(base.base_plugin):
pubsub.append(publish)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result is False or result['type'] == 'error': return False
@@ -236,7 +236,7 @@ class xep_0060(base.base_plugin):
pubsub.append(retract)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result is False or result['type'] == 'error': return False
@@ -287,7 +287,7 @@ class xep_0060(base.base_plugin):
pubsub.append(affs)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = ps_jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result is False or result['type'] == 'error':
diff --git a/sleekxmpp/plugins/xep_0092.py b/sleekxmpp/plugins/xep_0092.py
index ca02c4a8..c9b418ff 100644
--- a/sleekxmpp/plugins/xep_0092.py
+++ b/sleekxmpp/plugins/xep_0092.py
@@ -42,7 +42,7 @@ class xep_0092(base.base_plugin):
query = ET.Element('{jabber:iq:version}query')
iq.append(query)
iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.fulljid
+ iq.attrib['from'] = self.xmpp.boundjid.full
id = iq.get('id')
result = iq.send()
if result and result is not None and result.get('type', 'error') != 'error':
diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py
index fc7aff34..9e91b5d8 100644
--- a/sleekxmpp/xmlstream/xmlstream.py
+++ b/sleekxmpp/xmlstream/xmlstream.py
@@ -94,6 +94,8 @@ class XMLStream(object):
ssl_support -- Indicates if a SSL library is available for use.
ssl_version -- The version of the SSL protocol to use.
Defaults to ssl.PROTOCOL_TLSv1.
+ ca_certs -- File path to a CA certificate to verify the
+ server's identity.
state -- A state machine for managing the stream's
connection state.
stream_footer -- The start tag and any attributes for the stream's
@@ -163,6 +165,7 @@ class XMLStream(object):
self.ssl_support = SSL_SUPPORT
self.ssl_version = ssl.PROTOCOL_TLSv1
+ self.ca_certs = None
self.response_timeout = RESPONSE_TIMEOUT
@@ -283,7 +286,15 @@ class XMLStream(object):
self.socket.settimeout(None)
if self.use_ssl and self.ssl_support:
log.debug("Socket Wrapped for SSL")
- ssl_socket = ssl.wrap_socket(self.socket)
+ if self.ca_certs is None:
+ cert_policy = ssl.CERT_NONE
+ else:
+ cert_policy = ssl.CERT_REQUIRED
+
+ ssl_socket = ssl.wrap_socket(self.socket,
+ ca_certs=self.ca_certs,
+ certs_reqs=cert_policy)
+
if hasattr(self.socket, 'socket'):
# We are using a testing socket, so preserve the top
# layer of wrapping.
@@ -387,9 +398,17 @@ class XMLStream(object):
if self.ssl_support:
log.info("Negotiating TLS")
log.info("Using SSL version: %s" % str(self.ssl_version))
+ if self.ca_certs is None:
+ cert_policy = ssl.CERT_NONE
+ else:
+ cert_policy = ssl.CERT_REQUIRED
+
ssl_socket = ssl.wrap_socket(self.socket,
ssl_version=self.ssl_version,
- do_handshake_on_connect=False)
+ do_handshake_on_connect=False,
+ ca_certs=self.ca_certs,
+ cert_reqs=cert_policy)
+
if hasattr(self.socket, 'socket'):
# We are using a testing socket, so preserve the top
# layer of wrapping.