diff options
author | Lance Stout <lancestout@gmail.com> | 2010-12-16 22:03:56 -0500 |
---|---|---|
committer | Lance Stout <lancestout@gmail.com> | 2010-12-16 22:03:56 -0500 |
commit | adade2e5eccf5a0c48b0b6541fc3d990d732710c (patch) | |
tree | 9d5bd167058070bfe2b90a7a093e53978a4e29ed | |
parent | c16913c99929a6a5a57611ec8a1757e3e82d4a45 (diff) | |
parent | cbc42c29fb02a6fd22a0c303e8d02364545c86cf (diff) | |
download | slixmpp-adade2e5eccf5a0c48b0b6541fc3d990d732710c.tar.gz slixmpp-adade2e5eccf5a0c48b0b6541fc3d990d732710c.tar.bz2 slixmpp-adade2e5eccf5a0c48b0b6541fc3d990d732710c.tar.xz slixmpp-adade2e5eccf5a0c48b0b6541fc3d990d732710c.zip |
Merge branch 'develop' into roster
-rwxr-xr-x | examples/disco_browser.py | 198 | ||||
-rwxr-xr-x | examples/echo_client.py | 7 | ||||
-rw-r--r-- | setup.py | 4 | ||||
-rw-r--r-- | sleekxmpp/basexmpp.py | 99 | ||||
-rw-r--r-- | sleekxmpp/plugins/__init__.py | 6 | ||||
-rw-r--r-- | sleekxmpp/plugins/gmail_notify.py | 2 | ||||
-rw-r--r-- | sleekxmpp/plugins/jobs.py | 3 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0004.py | 3 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/disco.py | 366 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/stanza/disco.py | 0 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/stanza/items.py | 4 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/static.py | 49 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0045.py | 648 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0050.py | 2 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0060.py | 30 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0092.py | 2 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/xmlstream.py | 23 |
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 @@ -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. |