summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.rst28
-rw-r--r--docs/.gitignore1
-rw-r--r--docs/_static/haiku.css35
-rw-r--r--docs/_static/images/from_&yet.pngbin0 -> 2812 bytes
-rw-r--r--docs/_static/pygments.css70
-rw-r--r--docs/_templates/layout.html70
-rw-r--r--docs/conf.py4
-rw-r--r--docs/getting_started/muc.rst206
-rw-r--r--docs/index.rst93
-rwxr-xr-xexamples/muc.py4
-rw-r--r--examples/pubsub_client.py198
-rw-r--r--examples/pubsub_events.py151
-rw-r--r--examples/register_account.py175
-rw-r--r--examples/thirdpary_auth.py247
-rw-r--r--examples/user_location.py125
-rw-r--r--examples/user_tune.py137
-rwxr-xr-xsetup.py14
-rw-r--r--sleekxmpp/basexmpp.py89
-rw-r--r--sleekxmpp/clientxmpp.py69
-rw-r--r--sleekxmpp/componentxmpp.py11
-rw-r--r--sleekxmpp/features/__init__.py8
-rw-r--r--sleekxmpp/features/feature_bind/__init__.py11
-rw-r--r--sleekxmpp/features/feature_bind/bind.py17
-rw-r--r--sleekxmpp/features/feature_bind/stanza.py3
-rw-r--r--sleekxmpp/features/feature_mechanisms/__init__.py11
-rw-r--r--sleekxmpp/features/feature_mechanisms/mechanisms.py84
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/__init__.py1
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/abort.py24
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/auth.py21
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/challenge.py9
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/failure.py4
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py4
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/response.py9
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/success.py4
-rw-r--r--sleekxmpp/features/feature_rosterver/__init__.py19
-rw-r--r--sleekxmpp/features/feature_rosterver/rosterver.py42
-rw-r--r--sleekxmpp/features/feature_rosterver/stanza.py17
-rw-r--r--sleekxmpp/features/feature_session/__init__.py11
-rw-r--r--sleekxmpp/features/feature_session/session.py18
-rw-r--r--sleekxmpp/features/feature_session/stanza.py3
-rw-r--r--sleekxmpp/features/feature_starttls/__init__.py11
-rw-r--r--sleekxmpp/features/feature_starttls/stanza.py2
-rw-r--r--sleekxmpp/features/feature_starttls/starttls.py18
-rw-r--r--sleekxmpp/plugins/__init__.py47
-rw-r--r--sleekxmpp/plugins/base.py340
-rw-r--r--sleekxmpp/plugins/xep_0004/__init__.py13
-rw-r--r--sleekxmpp/plugins/xep_0004/dataforms.py30
-rw-r--r--sleekxmpp/plugins/xep_0004/stanza/field.py10
-rw-r--r--sleekxmpp/plugins/xep_0009/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0009/binding.py10
-rw-r--r--sleekxmpp/plugins/xep_0009/rpc.py31
-rw-r--r--sleekxmpp/plugins/xep_0012.py27
-rw-r--r--sleekxmpp/plugins/xep_0030/__init__.py13
-rw-r--r--sleekxmpp/plugins/xep_0030/disco.py279
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/info.py36
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/items.py45
-rw-r--r--sleekxmpp/plugins/xep_0030/static.py365
-rw-r--r--sleekxmpp/plugins/xep_0033.py248
-rw-r--r--sleekxmpp/plugins/xep_0045.py32
-rw-r--r--sleekxmpp/plugins/xep_0047/__init__.py21
-rw-r--r--sleekxmpp/plugins/xep_0047/ibb.py148
-rw-r--r--sleekxmpp/plugins/xep_0047/stanza.py67
-rw-r--r--sleekxmpp/plugins/xep_0047/stream.py137
-rw-r--r--sleekxmpp/plugins/xep_0050/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0050/adhoc.py18
-rw-r--r--sleekxmpp/plugins/xep_0059/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0059/rsm.py50
-rw-r--r--sleekxmpp/plugins/xep_0060/__init__.py19
-rw-r--r--sleekxmpp/plugins/xep_0060/pubsub.py162
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py47
-rw-r--r--sleekxmpp/plugins/xep_0066/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0066/oob.py21
-rw-r--r--sleekxmpp/plugins/xep_0077/__init__.py19
-rw-r--r--sleekxmpp/plugins/xep_0077/register.py90
-rw-r--r--sleekxmpp/plugins/xep_0077/stanza.py73
-rw-r--r--sleekxmpp/plugins/xep_0078/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0078/legacyauth.py16
-rw-r--r--sleekxmpp/plugins/xep_0080/__init__.py15
-rw-r--r--sleekxmpp/plugins/xep_0080/geoloc.py122
-rw-r--r--sleekxmpp/plugins/xep_0080/stanza.py266
-rw-r--r--sleekxmpp/plugins/xep_0082.py15
-rw-r--r--sleekxmpp/plugins/xep_0085/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0085/chat_states.py30
-rw-r--r--sleekxmpp/plugins/xep_0085/stanza.py21
-rw-r--r--sleekxmpp/plugins/xep_0086/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0086/legacy_error.py13
-rw-r--r--sleekxmpp/plugins/xep_0092/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0092/version.py20
-rw-r--r--sleekxmpp/plugins/xep_0107/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0107/stanza.py55
-rw-r--r--sleekxmpp/plugins/xep_0107/user_mood.py87
-rw-r--r--sleekxmpp/plugins/xep_0108/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0108/stanza.py83
-rw-r--r--sleekxmpp/plugins/xep_0108/user_activity.py84
-rw-r--r--sleekxmpp/plugins/xep_0115/__init__.py20
-rw-r--r--sleekxmpp/plugins/xep_0115/caps.py305
-rw-r--r--sleekxmpp/plugins/xep_0115/stanza.py19
-rw-r--r--sleekxmpp/plugins/xep_0115/static.py146
-rw-r--r--sleekxmpp/plugins/xep_0118/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0118/stanza.py25
-rw-r--r--sleekxmpp/plugins/xep_0118/user_tune.py92
-rw-r--r--sleekxmpp/plugins/xep_0128/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0128/extended_disco.py18
-rw-r--r--sleekxmpp/plugins/xep_0128/static.py37
-rw-r--r--sleekxmpp/plugins/xep_0163.py120
-rw-r--r--sleekxmpp/plugins/xep_0172/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0172/stanza.py67
-rw-r--r--sleekxmpp/plugins/xep_0172/user_nick.py86
-rw-r--r--sleekxmpp/plugins/xep_0184/__init__.py19
-rw-r--r--sleekxmpp/plugins/xep_0184/receipt.py120
-rw-r--r--sleekxmpp/plugins/xep_0184/stanza.py72
-rw-r--r--sleekxmpp/plugins/xep_0198/__init__.py20
-rw-r--r--sleekxmpp/plugins/xep_0198/stanza.py151
-rw-r--r--sleekxmpp/plugins/xep_0198/stream_management.py266
-rw-r--r--sleekxmpp/plugins/xep_0199/__init__.py12
-rw-r--r--sleekxmpp/plugins/xep_0199/ping.py20
-rw-r--r--sleekxmpp/plugins/xep_0202/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0202/time.py17
-rw-r--r--sleekxmpp/plugins/xep_0203/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0203/delay.py13
-rw-r--r--sleekxmpp/plugins/xep_0224/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0224/attention.py16
-rw-r--r--sleekxmpp/plugins/xep_0249/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0249/invite.py17
-rw-r--r--sleekxmpp/roster/item.py23
-rw-r--r--sleekxmpp/roster/multi.py19
-rw-r--r--sleekxmpp/roster/single.py62
-rw-r--r--sleekxmpp/stanza/error.py2
-rw-r--r--sleekxmpp/stanza/iq.py1
-rw-r--r--sleekxmpp/stanza/message.py3
-rw-r--r--sleekxmpp/stanza/nick.py65
-rw-r--r--sleekxmpp/stanza/presence.py3
-rw-r--r--sleekxmpp/stanza/rootstanza.py2
-rw-r--r--sleekxmpp/stanza/roster.py102
-rw-r--r--sleekxmpp/stanza/stream_error.py3
-rw-r--r--sleekxmpp/stanza/stream_features.py3
-rw-r--r--sleekxmpp/test/sleektest.py6
-rw-r--r--sleekxmpp/thirdparty/mini_dateutil.py5
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/__init__.py3
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py2
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py2
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py39
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/google_token.py22
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py17
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/plain.py2
-rw-r--r--sleekxmpp/thirdparty/suelta/saslprep.py2
-rw-r--r--sleekxmpp/version.py4
-rw-r--r--sleekxmpp/xmlstream/handler/waiter.py1
-rw-r--r--sleekxmpp/xmlstream/jid.py4
-rw-r--r--sleekxmpp/xmlstream/matcher/stanzapath.py11
-rw-r--r--sleekxmpp/xmlstream/scheduler.py2
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py98
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py212
-rw-r--r--tests/test_plugins.py162
-rw-r--r--tests/test_stanza_xep_0009.py26
-rw-r--r--tests/test_stanza_xep_0047.py90
-rw-r--r--tests/test_stanza_xep_0085.py6
-rw-r--r--tests/test_stanza_xep_0184.py38
-rw-r--r--tests/test_stream_filters.py88
-rw-r--r--tests/test_stream_presence.py12
-rw-r--r--tests/test_stream_roster.py132
-rw-r--r--tests/test_stream_xep_0030.py16
-rw-r--r--tests/test_stream_xep_0047.py180
164 files changed, 7588 insertions, 1068 deletions
diff --git a/.gitignore b/.gitignore
index 9b7733be..2c491a83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ docs/_build/
*.swp
.tox/
.coverage
+sleekxmpp.egg-info/
diff --git a/README.rst b/README.rst
index e2c289d9..a477a3de 100644
--- a/README.rst
+++ b/README.rst
@@ -118,8 +118,12 @@ SleekXMPP projects::
self.add_event_handler("session_start", self.session_start)
self.add_event_handler("message", self.message)
- self.register_plugin('xep_0030') # Service Discovery
- self.register_plugin('xep_0199') # XMPP Ping
+ # If you wanted more functionality, here's how to register plugins:
+ # self.register_plugin('xep_0030') # Service Discovery
+ # self.register_plugin('xep_0199') # XMPP Ping
+
+ # Here's how to access plugins once you've registered them:
+ # self['xep_0030'].add_feature('echo_demo')
# If you are working with an OpenFire server, you will
# need to use a different SSL version:
@@ -128,18 +132,20 @@ SleekXMPP projects::
def session_start(self, event):
self.send_presence()
+ self.get_roster()
# Most get_*/set_* methods from plugins use Iq stanzas, which
# can generate IqError and IqTimeout exceptions
- try:
- self.get_roster()
- except IqError as err:
- logging.error('There was an error getting the roster')
- logging.error(err.iq['error']['condition'])
- self.disconnect()
- except IqTimeout:
- logging.error('Server is taking too long to respond')
- self.disconnect()
+ #
+ # try:
+ # self.get_roster()
+ # except IqError as err:
+ # logging.error('There was an error getting the roster')
+ # logging.error(err.iq['error']['condition'])
+ # self.disconnect()
+ # except IqTimeout:
+ # logging.error('Server is taking too long to respond')
+ # self.disconnect()
def message(self, msg):
if msg['type'] in ('chat', 'normal'):
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000..88f9974b
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1 @@
+_build/*
diff --git a/docs/_static/haiku.css b/docs/_static/haiku.css
index 615ed47b..3d8ee6a7 100644
--- a/docs/_static/haiku.css
+++ b/docs/_static/haiku.css
@@ -59,9 +59,10 @@ body {
margin: auto;
padding: 0px;
font-family: "Helvetica Neueu", Helvetica, sans-serif;
- min-width: 59em;
+ min-width: 30em;
max-width: 70em;
color: #444;
+ text-align: center;
}
div.footer {
@@ -124,21 +125,25 @@ a.headerlink:hover {
/* basic text elements */
div.content {
+ margin: auto;
margin-top: 20px;
- margin-left: 40px;
- margin-right: 40px;
margin-bottom: 50px;
font-size: 0.9em;
+ width: 700px;
+ text-align: left;
}
/* heading and navigation */
div.header {
position: relative;
+ margin: auto;
margin-top: 125px;
height: 85px;
padding: 0 40px;
font-family: "Yanone Kaffeesatz";
+ text-align: left;
+ width: 750px;
}
div.header h1 {
font-size: 2.6em;
@@ -185,12 +190,12 @@ div.topnav {
z-index: 0;
}
div.topnav p {
+ margin: auto;
margin-top: 0;
- margin-left: 40px;
- margin-right: 40px;
margin-bottom: 0px;
text-align: right;
font-size: 0.8em;
+ width: 750px;
}
div.bottomnav {
background: #eeeeee;
@@ -404,3 +409,23 @@ div.viewcode-block:target {
padding: 0 12px;
}
+#from_andyet {
+ -webkit-box-shadow: #CCC 0px 0px 3px;
+ background: rgba(255, 255, 255, 1);
+ bottom: 0px;
+ right: 17px;
+ padding: 3px 10px;
+ position: fixed;
+}
+
+#from_andyet h2 {
+ background-image: url("images/from_&yet.png");
+ background-repeat: no-repeat;
+ height: 29px;
+ line-height: 0;
+ text-indent: -9999em;
+ width: 79px;
+ margin-top: 0;
+ margin: 0px;
+ padding: 0px;
+}
diff --git a/docs/_static/images/from_&yet.png b/docs/_static/images/from_&yet.png
new file mode 100644
index 00000000..ed5d8f33
--- /dev/null
+++ b/docs/_static/images/from_&yet.png
Binary files differ
diff --git a/docs/_static/pygments.css b/docs/_static/pygments.css
new file mode 100644
index 00000000..f04bc738
--- /dev/null
+++ b/docs/_static/pygments.css
@@ -0,0 +1,70 @@
+.highlight .hll { background-color: #ffffcc }
+.highlight { background: #000000; color: #f6f3e8; }
+.highlight .c { color: #7C7C7C; } /* Comment */
+.highlight .err { color: #f6f3e8; } /* Error */
+.highlight .g { color: #f6f3e8; } /* Generic */
+.highlight .k { color: #00ADEE; } /* Keyword */
+.highlight .l { color: #f6f3e8; } /* Literal */
+.highlight .n { color: #f6f3e8; } /* Name */
+.highlight .o { color: #f6f3e8; } /* Operator */
+.highlight .x { color: #f6f3e8; } /* Other */
+.highlight .p { color: #f6f3e8; } /* Punctuation */
+.highlight .cm { color: #7C7C7C; } /* Comment.Multiline */
+.highlight .cp { color: #96CBFE; } /* Comment.Preproc */
+.highlight .c1 { color: #7C7C7C; } /* Comment.Single */
+.highlight .cs { color: #7C7C7C; } /* Comment.Special */
+.highlight .gd { color: #f6f3e8; } /* Generic.Deleted */
+.highlight .ge { color: #f6f3e8; } /* Generic.Emph */
+.highlight .gr { color: #ffffff; background-color: #ff0000 } /* Generic.Error */
+.highlight .gh { color: #f6f3e8; font-weight: bold; } /* Generic.Heading */
+.highlight .gi { color: #f6f3e8; } /* Generic.Inserted */
+.highlight .go { color: #070707; } /* Generic.Output */
+.highlight .gp { color: #f6f3e8; } /* Generic.Prompt */
+.highlight .gs { color: #f6f3e8; } /* Generic.Strong */
+.highlight .gu { color: #f6f3e8; font-weight: bold; } /* Generic.Subheading */
+.highlight .gt { color: #ffffff; font-weight: bold; background-color: #FF6C60 } /* Generic.Traceback */
+.highlight .kc { color: #6699CC; } /* Keyword.Constant */
+.highlight .kd { color: #6699CC; } /* Keyword.Declaration */
+.highlight .kn { color: #6699CC; } /* Keyword.Namespace */
+.highlight .kp { color: #6699CC; } /* Keyword.Pseudo */
+.highlight .kr { color: #6699CC; } /* Keyword.Reserved */
+.highlight .kt { color: #FFFFB6; } /* Keyword.Type */
+.highlight .ld { color: #f6f3e8; } /* Literal.Date */
+.highlight .m { color: #FF73FD; } /* Literal.Number */
+.highlight .s { color: #F46DBA;/*#A8FF60;*/ } /* Literal.String */
+.highlight .na { color: #f6f3e8; } /* Name.Attribute */
+.highlight .nb { color: #f6f3e8; } /* Name.Builtin */
+.highlight .nc { color: #f6f3e8; } /* Name.Class */
+.highlight .no { color: #99CC99; } /* Name.Constant */
+.highlight .nd { color: #f6f3e8; } /* Name.Decorator */
+.highlight .ni { color: #E18964; } /* Name.Entity */
+.highlight .ne { color: #f6f3e8; } /* Name.Exception */
+.highlight .nf { color: #F64DBA; } /* Name.Function */
+.highlight .nl { color: #f6f3e8; } /* Name.Label */
+.highlight .nn { color: #f6f3e8; } /* Name.Namespace */
+.highlight .nx { color: #f6f3e8; } /* Name.Other */
+.highlight .py { color: #f6f3e8; } /* Name.Property */
+.highlight .nt { color: #00ADEE; } /* Name.Tag */
+.highlight .nv { color: #C6C5FE; } /* Name.Variable */
+.highlight .ow { color: #ffffff; } /* Operator.Word */
+.highlight .w { color: #f6f3e8; } /* Text.Whitespace */
+.highlight .mf { color: #FF73FD; } /* Literal.Number.Float */
+.highlight .mh { color: #FF73FD; } /* Literal.Number.Hex */
+.highlight .mi { color: #FF73FD; } /* Literal.Number.Integer */
+.highlight .mo { color: #FF73FD; } /* Literal.Number.Oct */
+.highlight .sb { color: #A8FF60; } /* Literal.String.Backtick */
+.highlight .sc { color: #A8FF60; } /* Literal.String.Char */
+.highlight .sd { color: #A8FF60; } /* Literal.String.Doc */
+.highlight .s2 { color: #A8FF60; } /* Literal.String.Double */
+.highlight .se { color: #A8FF60; } /* Literal.String.Escape */
+.highlight .sh { color: #A8FF60; } /* Literal.String.Heredoc */
+.highlight .si { color: #A8FF60; } /* Literal.String.Interpol */
+.highlight .sx { color: #A8FF60; } /* Literal.String.Other */
+.highlight .sr { color: #A8FF60; } /* Literal.String.Regex */
+.highlight .s1 { color: #A8FF60; } /* Literal.String.Single */
+.highlight .ss { color: #A8FF60; } /* Literal.String.Symbol */
+.highlight .bp { color: #f6f3e8; } /* Name.Builtin.Pseudo */
+.highlight .vc { color: #C6C5FE; } /* Name.Variable.Class */
+.highlight .vg { color: #C6C5FE; } /* Name.Variable.Global */
+.highlight .vi { color: #C6C5FE; } /* Name.Variable.Instance */
+.highlight .il { color: #FF73FD; } /* Literal.Number.Integer.Long */
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
new file mode 100644
index 00000000..de6f7244
--- /dev/null
+++ b/docs/_templates/layout.html
@@ -0,0 +1,70 @@
+{#
+ haiku/layout.html
+ ~~~~~~~~~~~~~~~~~
+
+ Sphinx layout template for the haiku theme.
+
+ :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+#}
+{% extends "basic/layout.html" %}
+{% set script_files = script_files + ['_static/theme_extras.js'] %}
+{% set css_files = css_files + ['_static/print.css'] %}
+
+{# do not display relbars #}
+{% block relbar1 %}{% endblock %}
+{% block relbar2 %}{% endblock %}
+
+{% macro nav() %}
+ <p>
+ {%- block haikurel1 %}
+ {%- endblock %}
+ {%- if prev %}
+ «&#160;&#160;<a href="{{ prev.link|e }}">{{ prev.title }}</a>
+ &#160;&#160;::&#160;&#160;
+ {%- endif %}
+ <a class="uplink" href="{{ pathto(master_doc) }}">{{ _('Contents') }}</a>
+ {%- if next %}
+ &#160;&#160;::&#160;&#160;
+ <a href="{{ next.link|e }}">{{ next.title }}</a>&#160;&#160;»
+ {%- endif %}
+ {%- block haikurel2 %}
+ {%- endblock %}
+ </p>
+{% endmacro %}
+
+{% block content %}
+ <div class="header">
+ {%- block haikuheader %}
+ {%- if theme_full_logo != "false" %}
+ <a href="{{ pathto('index') }}">
+ <img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
+ </a>
+ {%- else %}
+ {%- if logo -%}
+ <img class="rightlogo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
+ {%- endif -%}
+ <h1 class="heading"><a href="{{ pathto('index') }}">
+ <span>{{ title|striptags }}</span></a></h1>
+ <h2 class="heading"><span>{{ shorttitle|e }}</span></h2>
+ {%- endif %}
+ {%- endblock %}
+ </div>
+ <div class="topnav">
+ {{ nav() }}
+ </div>
+ <div class="content">
+ {#{%- if display_toc %}
+ <div id="toc">
+ <h3>Table Of Contents</h3>
+ {{ toc }}
+ </div>
+ {%- endif %}#}
+ {% block body %}{% endblock %}
+ </div>
+ <div class="bottomnav">
+ {{ nav() }}
+ </div>
+ <a id="from_andyet" href="http://andyet.net"><h2>From &amp;yet</h2></a>
+{% endblock %}
+
diff --git a/docs/conf.py b/docs/conf.py
index dd83f243..72e39d0f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -50,7 +50,7 @@ copyright = u'2011, Nathan Fritz, Lance Stout'
# The short X.Y version.
version = '1.0'
# The full version, including alpha/beta/rc tags.
-release = '1.0RC3'
+release = '1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -91,7 +91,7 @@ pygments_style = 'tango'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-html_theme = 'nature'
+html_theme = 'haiku'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
diff --git a/docs/getting_started/muc.rst b/docs/getting_started/muc.rst
index 08f721f7..26e1fa57 100644
--- a/docs/getting_started/muc.rst
+++ b/docs/getting_started/muc.rst
@@ -1,2 +1,208 @@
+.. _mucbot:
+
+=========================
Mulit-User Chat (MUC) Bot
=========================
+
+.. note::
+
+ If you have any issues working through this quickstart guide
+ or the other tutorials here, please either send a message to the
+ `mailing list <http://groups.google.com/group/sleekxmpp-discussion>`_
+ or join the chat room at `sleek@conference.jabber.org
+ <xmpp:sleek@conference.jabber.org?join>`_.
+
+If you have not yet installed SleekXMPP, do so now by either checking out a version
+from `Github <http://github.com/fritzy/SleekXMPP>`_, or installing it using ``pip``
+or ``easy_install``.
+
+.. code-block:: sh
+
+ pip install sleekxmpp # Or: easy_install sleekxmpp
+
+
+Now that you've got the basic gist of using SleekXMPP by following the
+echobot example (:ref:`echobot`), we can use one of the bundled plugins
+to create a very popular XMPP starter project: a `Multi-User Chat`_
+(MUC) bot. Our bot will login to an XMPP server, join an MUC chat room
+and "lurk" indefinitely, responding with a generic message to anyone
+that mentions its nickname. It will also greet members as they join the
+chat room.
+
+.. _`multi-user chat`: http://xmpp.org/extensions/xep-0045.html
+
+Joining The Room
+----------------
+
+As usual, our code will be based on the pattern explained in :ref:`echobot`.
+To start, we create an ``MUCBot`` class based on
+:class:`ClientXMPP <sleekxmpp.clientxmpp.ClientXMPP>` and which accepts
+parameters for the JID of the MUC room to join, and the nick that the
+bot will use inside the chat room. We also register an
+:term:`event handler` for the :term:`session_start` event.
+
+
+.. code-block:: python
+
+ import sleekxmpp
+
+ class MUCBot(sleekxmpp.ClientXMPP):
+
+ def __init__(self, jid, password, room, nick):
+ sleekxmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.room = room
+ self.nick = nick
+
+ self.add_event_handler("session_start", self.start)
+
+After initialization, we also need to register the MUC (XEP-0045) plugin
+so that we can make use of the group chat plugin's methods and events.
+
+.. code-block:: python
+
+ xmpp.register_plugin('xep_0045')
+
+Finally, we can make our bot join the chat room once an XMPP session
+has been established:
+
+.. code-block:: python
+
+ def start(self, event):
+ self.get_roster()
+ self.send_presence()
+ self.plugin['xep_0045'].joinMUC(self.room,
+ self.nick,
+ wait=True)
+
+Note that as in :ref:`echobot`, we need to include send an initial presence and request
+the roster. Next, we want to join the group chat, so we call the
+``joinMUC`` method of the MUC plugin.
+
+.. note::
+
+ The :attr:`plugin <sleekxmpp.basexmpp.BaseXMPP.plugin>` attribute is
+ dictionary that maps to instances of plugins that we have previously
+ registered, by their names.
+
+
+Adding Functionality
+--------------------
+
+Currently, our bot just sits dormantly inside the chat room, but we
+would like it to respond to two distinct events by issuing a generic
+message in each case to the chat room. In particular, when a member
+mentions the bot's nickname inside the chat room, and when a member
+joins the chat room.
+
+Responding to Mentions
+~~~~~~~~~~~~~~~~~~~~~~
+
+Whenever a user mentions our bot's nickname in chat, our bot will
+respond with a generic message resembling *"I heard that, user."* We do
+this by examining all of the messages sent inside the chat and looking
+for the ones which contain the nickname string.
+
+First, we register an event handler for the :term:`groupchat_message`
+event inside the bot's ``__init__`` function.
+
+.. note::
+
+ We do not register a handler for the :term:`message` event in this
+ bot, but if we did, the group chat message would have been sent to
+ both handlers.
+
+.. code-block:: python
+
+ def __init__(self, jid, password, room, nick):
+ sleekxmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.room = room
+ self.nick = nick
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("groupchat_message", self.muc_message)
+
+Then, we can send our generic message whenever the bot's nickname gets
+mentioned.
+
+.. warning::
+
+ Always check that a message is not from yourself,
+ otherwise you will create an infinite loop responding
+ to your own messages.
+
+.. code-block:: python
+
+ def muc_message(self, msg):
+ if msg['mucnick'] != self.nick and self.nick in msg['body']:
+ self.send_message(mto=msg['from'].bare,
+ mbody="I heard that, %s." % msg['mucnick'],
+ mtype='groupchat')
+
+
+Greeting Members
+~~~~~~~~~~~~~~~~
+
+Now we want to greet member whenever they join the group chat. To
+do this we will use the dynamic ``muc::room@server::got_online`` [1]_
+event so it's a good idea to register an event handler for it.
+
+.. note::
+
+ The groupchat_presence event is triggered whenever a
+ presence stanza is received from any chat room, including
+ any presences you send yourself. To limit event handling
+ to a single room, use the events ``muc::room@server::presence``,
+ ``muc::room@server::got_online``, or ``muc::room@server::got_offline``.
+
+.. code-block:: python
+
+ def __init__(self, jid, password, room, nick):
+ sleekxmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.room = room
+ self.nick = nick
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("groupchat_message", self.muc_message)
+ self.add_event_handler("muc::%s::got_online" % self.room,
+ self.muc_online)
+
+Now all that's left to do is to greet them:
+
+.. code-block:: python
+
+ def muc_online(self, presence):
+ if presence['muc']['nick'] != self.nick:
+ self.send_message(mto=presence['from'].bare,
+ mbody="Hello, %s %s" % (presence['muc']['role'],
+ presence['muc']['nick']),
+ mtype='groupchat')
+
+.. [1] this is similar to the :term:`got_online` event and is sent by
+ the xep_0045 plugin whenever a member joins the referenced
+ MUC chat room.
+
+
+Final Product
+-------------
+
+.. compound::
+
+ The final step is to create a small runner script for initialising our ``MUCBot`` class and adding some
+ basic configuration options. By following the basic boilerplate pattern in :ref:`echobot`, we arrive
+ at the code below. To experiment with this example, you can use:
+
+ .. code-block:: sh
+
+ python muc.py -d -j jid@example.com -r room@muc.example.net -n lurkbot
+
+ which will prompt for the password, log in, and join the group chat. To test, open
+ your regular IM client and join the same group chat that you sent the bot to. You
+ will see ``lurkbot`` as one of the members in the group chat, and that it greeted
+ you upon entry. Send a message with the string "lurkbot" inside the body text, and you
+ will also see that it responds with our pre-programmed customized message.
+
+.. include:: ../../examples/muc.py
+ :literal:
diff --git a/docs/index.rst b/docs/index.rst
index fc6541d6..6f6d8913 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -13,7 +13,7 @@ SleekXMPP
``develop`` branch.
**Latest Stable Release**
- - `1.0 RC3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC3>`_
+ - `1.0 <http://github.com/fritzy/SleekXMPP/zipball/1.0>`_
**Develop Releases**
- `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_
@@ -59,6 +59,72 @@ SleekXMPP's design goals and philosphy are:
sensible defaults and appropriate abstractions. XML can be ugly to work
with, but it doesn't have to be that way.
+Here's your first SleekXMPP Bot:
+--------------------------------
+
+.. code-block:: python
+
+ import logging
+
+ from sleekxmpp import ClientXMPP
+ from sleekxmpp.exceptions import IqError, IqTimeout
+
+
+ class EchoBot(ClientXMPP):
+
+ def __init__(self, jid, password):
+ ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.session_start)
+ self.add_event_handler("message", self.message)
+
+ # If you wanted more functionality, here's how to register plugins:
+ # self.register_plugin('xep_0030') # Service Discovery
+ # self.register_plugin('xep_0199') # XMPP Ping
+
+ # Here's how to access plugins once you've registered them:
+ # self['xep_0030'].add_feature('echo_demo')
+
+ # If you are working with an OpenFire server, you will
+ # need to use a different SSL version:
+ # import ssl
+ # self.ssl_version = ssl.PROTOCOL_SSLv3
+
+ def session_start(self, event):
+ self.send_presence()
+ self.get_roster()
+
+ # Most get_*/set_* methods from plugins use Iq stanzas, which
+ # can generate IqError and IqTimeout exceptions
+ #
+ # try:
+ # self.get_roster()
+ # except IqError as err:
+ # logging.error('There was an error getting the roster')
+ # logging.error(err.iq['error']['condition'])
+ # self.disconnect()
+ # except IqTimeout:
+ # logging.error('Server is taking too long to respond')
+ # self.disconnect()
+
+ def message(self, msg):
+ if msg['type'] in ('chat', 'normal'):
+ msg.reply("Thanks for sending\n%(body)s" % msg).send()
+
+
+ if __name__ == '__main__':
+ # Ideally use optparse or argparse to get JID,
+ # password, and log level.
+
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(levelname)-8s %(message)s')
+
+ xmpp = EchoBot('somejid@example.com', 'use_getpass')
+ xmpp.connect()
+ xmpp.process(block=True)
+
+
+
Getting Started (with Examples)
-------------------------------
.. toctree::
@@ -156,17 +222,24 @@ Additional Info
Credits
-------
-**Main Author:** Nathan Fritz
- `fritzy@netflint.net <xmpp:fritzy@netflint.net?message>`_,
- `@fritzy <http://twitter.com/fritzy>`_
- Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP
- <http://code.google.com/p/seesmic-as3-xmpp/>`_, and a member of the XMPP
- Council.
+**Main Author:** `Nathan Fritz <http://andyet.net/team/fritzy>`_
+ `fritzy@netflint.net <xmpp:fritzy@netflint.net?message>`_,
+ `@fritzy <http://twitter.com/fritzy>`_
+
+ Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP
+ <http://code.google.com/p/seesmic-as3-xmpp/>`_, and a former member of the XMPP
+ Council.
+
+**Co-Author:** `Lance Stout <http://andyet.net/team/lance>`_
+ `lancestout@gmail.com <xmpp:lancestout@gmail.com?message>`_,
+ `@lancestout <http://twitter.com/lancestout>`_
+
+Both Fritzy and Lance work for `&yet <http://andyet.net>`_, which specializes in
+realtime web and XMPP applications.
-**Co-Author:** Lance Stout
- `lancestout@gmail.com <xmpp:lancestout@gmail.com?message>`_,
- `@lancestout <http://twitter.com/lancestout>`_
+ - `contact@andyet.net <mailto:contact@andyet.net>`_
+ - `XMPP Consulting <http://xmppconsulting.com>`_
**Contributors:**
- Brian Beggs (`macdiesel <http://github.com/macdiesel>`_)
diff --git a/examples/muc.py b/examples/muc.py
index 8fe2eb49..7b93da16 100755
--- a/examples/muc.py
+++ b/examples/muc.py
@@ -76,8 +76,8 @@ class MUCBot(sleekxmpp.ClientXMPP):
event does not provide any additional
data.
"""
- self.getRoster()
- self.sendPresence()
+ self.get_roster()
+ self.send_presence()
self.plugin['xep_0045'].joinMUC(self.room,
self.nick,
# If a room password is needed, use:
diff --git a/examples/pubsub_client.py b/examples/pubsub_client.py
new file mode 100644
index 00000000..b0459c84
--- /dev/null
+++ b/examples/pubsub_client.py
@@ -0,0 +1,198 @@
+import sys
+import logging
+import getpass
+from optparse import OptionParser
+
+import sleekxmpp
+from sleekxmpp.xmlstream import ET, tostring
+
+
+# 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')
+else:
+ raw_input = input
+
+
+class PubsubClient(sleekxmpp.ClientXMPP):
+
+ def __init__(self, jid, password, server,
+ node=None, action='list', data=''):
+ super(PubsubClient, self).__init__(jid, password)
+
+ self.register_plugin('xep_0030')
+ self.register_plugin('xep_0059')
+ self.register_plugin('xep_0060')
+
+ self.actions = ['nodes', 'create', 'delete',
+ 'publish', 'get', 'retract',
+ 'purge', 'subscribe', 'unsubscribe']
+
+ self.action = action
+ self.node = node
+ self.data = data
+ self.pubsub_server = server
+
+ self.add_event_handler('session_start', self.start)
+
+ def start(self, event):
+ self.get_roster()
+ self.send_presence()
+
+ try:
+ getattr(self, self.action)()
+ except:
+ logging.error('Could not execute: %s' % self.action)
+ self.disconnect()
+
+ def nodes(self):
+ try:
+ result = self['xep_0060'].get_nodes(self.pubsub_server, self.node)
+ for item in result['disco_items']['items']:
+ print(' - %s' % str(item))
+ except:
+ logging.error('Could not retrieve node list.')
+
+ def create(self):
+ try:
+ self['xep_0060'].create_node(self.pubsub_server, self.node)
+ except:
+ logging.error('Could not create node: %s' % self.node)
+
+ def delete(self):
+ try:
+ self['xep_0060'].delete_node(self.pubsub_server, self.node)
+ print('Deleted node: %s' % self.node)
+ except:
+ logging.error('Could not delete node: %s' % self.node)
+
+ def publish(self):
+ payload = ET.fromstring("<test xmlns='test'>%s</test>" % self.data)
+ try:
+ result = self['xep_0060'].publish(self.pubsub_server, self.node, payload=payload)
+ id = result['pubsub']['publish']['item']['id']
+ print('Published at item id: %s' % id)
+ except:
+ logging.error('Could not publish to: %s' % self.node)
+
+ def get(self):
+ try:
+ result = self['xep_0060'].get_item(self.pubsub_server, self.node, self.data)
+ for item in result['pubsub']['items']['substanzas']:
+ print('Retrieved item %s: %s' % (item['id'], tostring(item['payload'])))
+ except:
+ logging.error('Could not retrieve item %s from node %s' % (self.data, self.node))
+
+ def retract(self):
+ try:
+ result = self['xep_0060'].retract(self.pubsub_server, self.node, self.data)
+ print('Retracted item %s from node %s' % (self.data, self.node))
+ except:
+ logging.error('Could not retract item %s from node %s' % (self.data, self.node))
+
+ def purge(self):
+ try:
+ result = self['xep_0060'].purge(self.pubsub_server, self.node)
+ print('Purged all items from node %s' % self.node)
+ except:
+ logging.error('Could not purge items from node %s' % self.node)
+
+ def subscribe(self):
+ try:
+ result = self['xep_0060'].subscribe(self.pubsub_server, self.node)
+ print('Subscribed %s to node %s' % (self.boundjid.bare, self.node))
+ except:
+ logging.error('Could not subscribe %s to node %s' % (self.boundjid.bare, self.node))
+
+ def unsubscribe(self):
+ try:
+ result = self['xep_0060'].unsubscribe(self.pubsub_server, self.node)
+ print('Unsubscribed %s from node %s' % (self.boundjid.bare, self.node))
+ except:
+ logging.error('Could not unsubscribe %s from node %s' % (self.boundjid.bare, self.node))
+
+
+
+
+if __name__ == '__main__':
+ # Setup the command line arguments.
+ optp = OptionParser()
+ optp.version = '%%prog 0.1'
+ optp.usage = "Usage: %%prog [options] <jid> " + \
+ 'nodes|create|delete|purge|subscribe|unsubscribe|publish|retract|get' + \
+ ' [<node> <data>]'
+
+ 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 opts.jid is None:
+ opts.jid = raw_input("Username: ")
+ if opts.password is None:
+ opts.password = getpass.getpass("Password: ")
+
+ if len(args) == 2:
+ args = (args[0], args[1], '', '', '')
+ elif len(args) == 3:
+ args = (args[0], args[1], args[2], '', '')
+ elif len(args) == 4:
+ args = (args[0], args[1], args[2], args[3], '')
+
+
+ # Setup the Pubsub client
+ xmpp = PubsubClient(opts.jid, opts.password,
+ server=args[0],
+ node=args[2],
+ action=args[1],
+ data=args[3])
+
+ # 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 dnspython 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(block=True)
+ else:
+ print("Unable to connect.")
diff --git a/examples/pubsub_events.py b/examples/pubsub_events.py
new file mode 100644
index 00000000..6fe7159b
--- /dev/null
+++ b/examples/pubsub_events.py
@@ -0,0 +1,151 @@
+import sys
+import logging
+import getpass
+from optparse import OptionParser
+
+import sleekxmpp
+from sleekxmpp.xmlstream import ET, tostring
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream.handler import Callback
+
+
+# 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')
+else:
+ raw_input = input
+
+
+class PubsubEvents(sleekxmpp.ClientXMPP):
+
+ def __init__(self, jid, password):
+ super(PubsubEvents, self).__init__(jid, password)
+
+ self.register_plugin('xep_0030')
+ self.register_plugin('xep_0059')
+ self.register_plugin('xep_0060')
+
+ self.add_event_handler('session_start', self.start)
+
+ # Some services may require configuration to allow
+ # sending delete, configuration, or subscription events.
+ self.add_event_handler('pubsub_publish', self._publish)
+ self.add_event_handler('pubsub_retract', self._retract)
+ self.add_event_handler('pubsub_purge', self._purge)
+ self.add_event_handler('pubsub_delete', self._delete)
+ self.add_event_handler('pubsub_config', self._config)
+ self.add_event_handler('pubsub_subscription', self._subscription)
+
+ # Want to use nicer, more specific pubsub event names?
+ # self['xep_0060'].map_node_event('node_name', 'event_prefix')
+ # self.add_event_handler('event_prefix_publish', handler)
+ # self.add_event_handler('event_prefix_retract', handler)
+ # self.add_event_handler('event_prefix_purge', handler)
+ # self.add_event_handler('event_prefix_delete', handler)
+
+ def start(self, event):
+ self.get_roster()
+ self.send_presence()
+
+ def _publish(self, msg):
+ """Handle receiving a publish item event."""
+ print('Published item %s to %s:' % (
+ msg['pubsub_event']['items']['item']['id'],
+ msg['pubsub_event']['items']['node']))
+ data = msg['pubsub_event']['items']['item']['payload']
+ if data is not None:
+ print(tostring(data))
+ else:
+ print('No item content')
+
+ def _retract(self, msg):
+ """Handle receiving a retract item event."""
+ print('Retracted item %s from %s' % (
+ msg['pubsub_event']['items']['retract']['id'],
+ msg['pubsub_event']['items']['node']))
+
+ def _purge(self, msg):
+ """Handle receiving a node purge event."""
+ print('Purged all items from %s' % (
+ msg['pubsub_event']['purge']['node']))
+
+ def _delete(self, msg):
+ """Handle receiving a node deletion event."""
+ print('Deleted node %s' % (
+ msg['pubsub_event']['delete']['node']))
+
+ def _config(self, msg):
+ """Handle receiving a node configuration event."""
+ print('Configured node %s:' % (
+ msg['pubsub_event']['configuration']['node']))
+ print(msg['pubsub_event']['configuration']['form'])
+
+ def _subscription(self, msg):
+ """Handle receiving a node subscription event."""
+ print('Subscription change for node %s:' % (
+ msg['pubsub_event']['subscription']['node']))
+ print(msg['pubsub_event']['subscription'])
+
+
+if __name__ == '__main__':
+ # Setup the command line arguments.
+ optp = OptionParser()
+
+ # Output verbosity options.
+ optp.add_option('-q', '--quiet', help='set logging to ERROR',
+ action='store_const', dest='loglevel',
+ const=logging.ERROR, default=logging.INFO)
+ optp.add_option('-d', '--debug', help='set logging to DEBUG',
+ action='store_const', dest='loglevel',
+ const=logging.DEBUG, default=logging.INFO)
+ optp.add_option('-v', '--verbose', help='set logging to COMM',
+ action='store_const', dest='loglevel',
+ const=5, default=logging.INFO)
+
+ # 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 opts.jid is None:
+ opts.jid = raw_input("Username: ")
+ if opts.password is None:
+ opts.password = getpass.getpass("Password: ")
+
+ logging.info("Run this in conjunction with the pubsub_client.py " + \
+ "example to watch events happen as you give commands.")
+
+ # Setup the PubsubEvents listener
+ xmpp = PubsubEvents(opts.jid, opts.password)
+
+ # 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 dnspython 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(block=True)
+ print("Done")
+ else:
+ print("Unable to connect.")
diff --git a/examples/register_account.py b/examples/register_account.py
new file mode 100644
index 00000000..17f075ab
--- /dev/null
+++ b/examples/register_account.py
@@ -0,0 +1,175 @@
+#!/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 logging
+import getpass
+from optparse import OptionParser
+
+import sleekxmpp
+from sleekxmpp.exceptions import IqError, IqTimeout
+
+# 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')
+else:
+ raw_input = input
+
+
+class RegisterBot(sleekxmpp.ClientXMPP):
+
+ """
+ A basic bot that will attempt to register an account
+ with an XMPP server.
+
+ NOTE: This follows the very basic registration workflow
+ from XEP-0077. More advanced server registration
+ workflows will need to check for data forms, etc.
+ """
+
+ def __init__(self, jid, password):
+ sleekxmpp.ClientXMPP.__init__(self, jid, password)
+
+ # 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 initialize
+ # our roster.
+ self.add_event_handler("session_start", self.start)
+
+ # The register event provides an Iq result stanza with
+ # a registration form from the server. This may include
+ # the basic registration fields, a data form, an
+ # out-of-band URL, or any combination. For more advanced
+ # cases, you will need to examine the fields provided
+ # and respond accordingly. SleekXMPP provides plugins
+ # for data forms and OOB links that will make that easier.
+ self.add_event_handler("register", self.register)
+
+ def start(self, event):
+ """
+ Process the session_start event.
+
+ Typical actions for the session_start event are
+ requesting the roster and broadcasting an initial
+ presence stanza.
+
+ Arguments:
+ event -- An empty dictionary. The session_start
+ event does not provide any additional
+ data.
+ """
+ self.send_presence()
+ self.get_roster()
+
+ # We're only concerned about registering, so nothing more to do here.
+ self.disconnect()
+
+ def register(self, iq):
+ """
+ Fill out and submit a registration form.
+
+ The form may be composed of basic registration fields, a data form,
+ an out-of-band link, or any combination thereof. Data forms and OOB
+ links can be checked for as so:
+
+ if iq.match('iq/register/form'):
+ # do stuff with data form
+ # iq['register']['form']['fields']
+ if iq.match('iq/register/oob'):
+ # do stuff with OOB URL
+ # iq['register']['oob']['url']
+
+ To get the list of basic registration fields, you can use:
+ iq['register']['fields']
+ """
+ resp = self.Iq()
+ resp['type'] = 'set'
+ resp['register']['username'] = self.boundjid.user
+ resp['register']['password'] = self.password
+
+ try:
+ resp.send(now=True)
+ logging.info("Account created for %s!" % self.boundjid)
+ except IqError as e:
+ logging.error("Could not register account: %s" %
+ e.iq['error']['text'])
+ self.disconnect()
+ except IqTimeout:
+ logging.error("No response from server.")
+ self.disconnect()
+
+
+if __name__ == '__main__':
+ # Setup the command line arguments.
+ optp = OptionParser()
+
+ # Output verbosity options.
+ optp.add_option('-q', '--quiet', help='set logging to ERROR',
+ action='store_const', dest='loglevel',
+ const=logging.ERROR, default=logging.INFO)
+ optp.add_option('-d', '--debug', help='set logging to DEBUG',
+ action='store_const', dest='loglevel',
+ const=logging.DEBUG, default=logging.INFO)
+ optp.add_option('-v', '--verbose', help='set logging to COMM',
+ action='store_const', dest='loglevel',
+ const=5, default=logging.INFO)
+
+ # 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 opts.jid is None:
+ opts.jid = raw_input("Username: ")
+ if opts.password is None:
+ opts.password = getpass.getpass("Password: ")
+
+ # Setup the RegisterBot and register plugins. Note that while plugins may
+ # have interdependencies, the order in which you register them does
+ # not matter.
+ xmpp = RegisterBot(opts.jid, opts.password)
+ xmpp.register_plugin('xep_0030') # Service Discovery
+ xmpp.register_plugin('xep_0004') # Data forms
+ xmpp.register_plugin('xep_0066') # Out-of-band Data
+ xmpp.register_plugin('xep_0077') # In-band Registration
+
+ # 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 dnspython 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(block=True)
+ print("Done")
+ else:
+ print("Unable to connect.")
diff --git a/examples/thirdpary_auth.py b/examples/thirdpary_auth.py
new file mode 100644
index 00000000..727311ae
--- /dev/null
+++ b/examples/thirdpary_auth.py
@@ -0,0 +1,247 @@
+#!/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 logging
+import getpass
+from optparse import OptionParser
+
+try:
+ from httplib import HTTPSConnection
+ from urllib import urlencode
+except ImportError:
+ from urllib.parse import urlencode
+ from http.client import HTTPSConnection
+
+import sleekxmpp
+from sleekxmpp.xmlstream import JID
+
+# 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')
+else:
+ raw_input = input
+
+
+class ThirdPartyAuthBot(sleekxmpp.ClientXMPP):
+
+ """
+ A simple SleekXMPP bot that will echo messages it
+ receives, along with a short thank you message.
+
+ This version uses a thirdpary service for authentication,
+ such as Facebook or Google.
+ """
+
+ def __init__(self, jid, password):
+ sleekxmpp.ClientXMPP.__init__(self, jid, password)
+
+ # The X-GOOGLE-TOKEN mech is ranked lower than PLAIN
+ # due to Google only allowing a single SASL attempt per
+ # connection. So PLAIN will be used for TLS connections,
+ # and X-GOOGLE-TOKEN for non-TLS connections. To use
+ # X-GOOGLE-TOKEN with a TLS connection, explicitly select
+ # it using:
+ #
+ # sleekxmpp.ClientXMPP.__init__(self, jid, password,
+ # sasl_mech="X-GOOGLE-TOKEN")
+
+ # 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 initialize
+ # our roster.
+ self.add_event_handler("session_start", self.start)
+
+ # The message event is triggered whenever a message
+ # stanza is received. Be aware that that includes
+ # MUC messages and error messages.
+ self.add_event_handler("message", self.message)
+
+ def start(self, event):
+ """
+ Process the session_start event.
+
+ Typical actions for the session_start event are
+ requesting the roster and broadcasting an initial
+ presence stanza.
+
+ Arguments:
+ event -- An empty dictionary. The session_start
+ event does not provide any additional
+ data.
+ """
+ self.send_presence()
+ self.get_roster()
+
+ def message(self, msg):
+ """
+ Process incoming message stanzas. Be aware that this also
+ includes MUC messages and error messages. It is usually
+ a good idea to check the messages's type before processing
+ or sending replies.
+
+ Arguments:
+ msg -- The received message stanza. See the documentation
+ for stanza objects and the Message stanza to see
+ how it may be used.
+ """
+ if msg['type'] in ('chat', 'normal'):
+ msg.reply("Thanks for sending\n%(body)s" % msg).send()
+
+
+if __name__ == '__main__':
+ # Setup the command line arguments.
+ optp = OptionParser()
+
+ # Output verbosity options.
+ optp.add_option('-q', '--quiet', help='set logging to ERROR',
+ action='store_const', dest='loglevel',
+ const=logging.ERROR, default=logging.INFO)
+ optp.add_option('-d', '--debug', help='set logging to DEBUG',
+ action='store_const', dest='loglevel',
+ const=logging.DEBUG, default=logging.INFO)
+ optp.add_option('-v', '--verbose', help='set logging to COMM',
+ action='store_const', dest='loglevel',
+ const=5, default=logging.INFO)
+
+ # 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 opts.jid is None:
+ opts.jid = raw_input("Username: ")
+ if opts.password is None:
+ opts.password = getpass.getpass("Password: ")
+
+
+ access_token = None
+
+ # Since documentation on how to work with Google tokens
+ # can be difficult to find, we'll demo a basic version
+ # here. Note that responses could refer to a Captcha
+ # URL that would require a browser.
+
+ # Using Facebook or MSN's custom authentication requires
+ # a browser, but the process is the same once a token
+ # has been retrieved.
+
+ # Request an access token from Google:
+ try:
+ conn = HTTPSConnection('www.google.com')
+ except:
+ print('Could not connect to Google')
+ sys.exit()
+
+ params = urlencode({
+ 'accountType': 'GOOGLE',
+ 'service': 'mail',
+ 'Email': JID(opts.jid).bare,
+ 'Passwd': opts.password
+ })
+ headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+ try:
+ conn.request('POST', '/accounts/ClientLogin', params, headers)
+ resp = conn.getresponse().read()
+ data = {}
+ for line in resp.split():
+ k, v = line.split(b'=', 1)
+ data[k] = v
+ except Exception as e:
+ print('Could not retrieve login data')
+ sys.exit()
+
+ if b'SID' not in data:
+ print('Required data not found')
+ sys.exit()
+
+
+ params = urlencode({
+ 'SID': data[b'SID'],
+ 'LSID': data[b'LSID'],
+ 'service': 'mail'
+ })
+ try:
+ conn.request('POST', '/accounts/IssueAuthToken', params, headers)
+ resp = conn.getresponse()
+ data = resp.read().split()
+ except:
+ print('Could not retrieve auth data')
+ sys.exit()
+
+ if not data:
+ print('Could not retrieve token')
+ sys.exit()
+
+ access_token = data[0]
+
+
+ # Setup the ThirdPartyAuthBot and register plugins. Note that while plugins
+ # may have interdependencies, the order in which you register them does not
+ # matter.
+
+ # If using MSN, the JID should be "user@messenger.live.com", which will
+ # be overridden on session bind.
+
+ # We're using an access token instead of a password, so we'll use `''` as
+ # a password argument filler.
+
+ xmpp = ThirdPartyAuthBot(opts.jid, '')
+ xmpp.credentials['access_token'] = access_token
+
+ # The credentials dictionary is used to provide additional authentication
+ # information beyond just a password.
+
+ xmpp.register_plugin('xep_0030') # Service Discovery
+ xmpp.register_plugin('xep_0004') # Data Forms
+ xmpp.register_plugin('xep_0060') # PubSub
+
+ # MSN will kill connections that have been inactive for even
+ # short periods of time. So use pings to keep the session alive;
+ # whitespace keepalives do not work.
+ xmpp.register_plugin('xep_0199', {'keepalive': True, 'frequency': 60})
+
+ # 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.
+ # Google only allows one SASL attempt per connection, so in order to
+ # enable the X-GOOGLE-TOKEN mechanism, we'll disable TLS.
+ if xmpp.connect(use_tls=False):
+ # If you do not have the dnspython 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(block=True)
+ print("Done")
+ else:
+ print("Unable to connect.")
diff --git a/examples/user_location.py b/examples/user_location.py
new file mode 100644
index 00000000..2877f7bd
--- /dev/null
+++ b/examples/user_location.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python
+
+import sys
+import logging
+import getpass
+from optparse import OptionParser
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+try:
+ import requests
+except ImportError:
+ print('This demo requires the requests package for using HTTP.')
+ sys.exit()
+
+from sleekxmpp import ClientXMPP
+
+
+class LocationBot(ClientXMPP):
+
+ def __init__(self, jid, password):
+ super(LocationBot, self).__init__(jid, password)
+
+ self.add_event_handler('session_start', self.start)
+ self.add_event_handler('user_location_publish',
+ self.user_location_publish)
+
+ self.register_plugin('xep_0004')
+ self.register_plugin('xep_0030')
+ self.register_plugin('xep_0060')
+ self.register_plugin('xep_0115')
+ self.register_plugin('xep_0128')
+ self.register_plugin('xep_0163')
+ self.register_plugin('xep_0080')
+
+ self.current_tune = None
+
+ def start(self, event):
+ self.send_presence()
+ self.get_roster()
+ self['xep_0115'].update_caps()
+
+ print("Using freegeoip.net to get geolocation.")
+ r = requests.get('http://freegeoip.net/json/')
+ try:
+ data = json.loads(r.text)
+ except:
+ print("Could not retrieve user location.")
+ self.disconnect()
+ return
+
+ self['xep_0080'].publish_location(
+ lat=data['latitude'],
+ lon=data['longitude'],
+ locality=data['city'],
+ region=data['region_name'],
+ country=data['country_name'],
+ countrycode=data['country_code'],
+ postalcode=data['zipcode'])
+
+ def user_location_publish(self, msg):
+ geo = msg['pubsub_event']['items']['item']['geoloc']
+ print("%s is at:" % msg['from'])
+ for key, val in geo.values.items():
+ if val:
+ print(" %s: %s" % (key, val))
+
+
+if __name__ == '__main__':
+ # Setup the command line arguments.
+ optp = OptionParser()
+
+ # Output verbosity options.
+ optp.add_option('-q', '--quiet', help='set logging to ERROR',
+ action='store_const', dest='loglevel',
+ const=logging.ERROR, default=logging.INFO)
+ optp.add_option('-d', '--debug', help='set logging to DEBUG',
+ action='store_const', dest='loglevel',
+ const=logging.DEBUG, default=logging.INFO)
+ optp.add_option('-v', '--verbose', help='set logging to COMM',
+ action='store_const', dest='loglevel',
+ const=5, default=logging.INFO)
+
+ # 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 opts.jid is None:
+ opts.jid = raw_input("Username: ")
+ if opts.password is None:
+ opts.password = getpass.getpass("Password: ")
+
+ xmpp = LocationBot(opts.jid, opts.password)
+
+ # 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 dnspython 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(block=True)
+ print("Done")
+ else:
+ print("Unable to connect.")
diff --git a/examples/user_tune.py b/examples/user_tune.py
new file mode 100644
index 00000000..09e050f0
--- /dev/null
+++ b/examples/user_tune.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+
+import sys
+import logging
+import getpass
+from optparse import OptionParser
+
+try:
+ from appscript import *
+except ImportError:
+ print('This demo requires the appscript package to interact with iTunes.')
+ sys.exit()
+
+from sleekxmpp import ClientXMPP
+
+
+class TuneBot(ClientXMPP):
+
+ def __init__(self, jid, password):
+ super(TuneBot, self).__init__(jid, password)
+
+ # Check for the current song every 5 seconds.
+ self.schedule('Check Current Tune', 5, self._update_tune, repeat=True)
+
+ self.add_event_handler('session_start', self.start)
+ self.add_event_handler('user_tune_publish', self.user_tune_publish)
+
+ self.register_plugin('xep_0004')
+ self.register_plugin('xep_0030')
+ self.register_plugin('xep_0060')
+ self.register_plugin('xep_0115')
+ self.register_plugin('xep_0118')
+ self.register_plugin('xep_0128')
+ self.register_plugin('xep_0163')
+
+ self.current_tune = None
+
+ def start(self, event):
+ self.send_presence()
+ self.get_roster()
+ self['xep_0115'].update_caps()
+
+ def _update_tune(self):
+ itunes_count = app('System Events').processes[its.name == 'iTunes'].count()
+ if itunes_count > 0:
+ iTunes = app('iTunes')
+ if iTunes.player_state.get() == k.playing:
+ track = iTunes.current_track.get()
+ length = track.time.get()
+ if ':' in length:
+ minutes, secs = map(int, length.split(':'))
+ secs += minutes * 60
+ else:
+ secs = int(length)
+
+ artist = track.artist.get()
+ title = track.name.get()
+ source = track.album.get()
+ rating = track.rating.get() / 10
+
+ tune = (artist, secs, rating, source, title)
+ if tune != self.current_tune:
+ self.current_tune = tune
+
+ # We have a new song playing, so publish it.
+ self['xep_0118'].publish_tune(
+ artist=artist,
+ length=secs,
+ title=title,
+ rating=rating,
+ source=source)
+ else:
+ # No song is playing, clear the user tune.
+ tune = None
+ if tune != self.current_tune:
+ self.current_tune = tune
+ self['xep_0118'].stop()
+
+ def user_tune_publish(self, msg):
+ tune = msg['pubsub_event']['items']['item']['tune']
+ print("%s is listening to: %s" % (msg['from'], tune['title']))
+
+
+if __name__ == '__main__':
+ # Setup the command line arguments.
+ optp = OptionParser()
+
+ # Output verbosity options.
+ optp.add_option('-q', '--quiet', help='set logging to ERROR',
+ action='store_const', dest='loglevel',
+ const=logging.ERROR, default=logging.INFO)
+ optp.add_option('-d', '--debug', help='set logging to DEBUG',
+ action='store_const', dest='loglevel',
+ const=logging.DEBUG, default=logging.INFO)
+ optp.add_option('-v', '--verbose', help='set logging to COMM',
+ action='store_const', dest='loglevel',
+ const=5, default=logging.INFO)
+
+ # 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 opts.jid is None:
+ opts.jid = raw_input("Username: ")
+ if opts.password is None:
+ opts.password = getpass.getpass("Password: ")
+
+ xmpp = TuneBot(opts.jid, opts.password)
+
+ # 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 dnspython 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(block=True)
+ print("Done")
+ else:
+ print("Unable to connect.")
diff --git a/setup.py b/setup.py
index a49ec9d6..670c4aff 100755
--- a/setup.py
+++ b/setup.py
@@ -8,6 +8,7 @@
# file, which you should have received as part of this distribution.
import sys
+import codecs
try:
from setuptools import setup, Command
except ImportError:
@@ -31,7 +32,7 @@ from sleekxmpp.version import __version__
VERSION = __version__
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
-with open('README.rst') as readme:
+with codecs.open('README.rst', 'r', encoding='UTF-8') as readme:
LONG_DESCRIPTION = ''.join(readme)
CLASSIFIERS = [ 'Intended Audience :: Developers',
@@ -58,16 +59,26 @@ packages = [ 'sleekxmpp',
'sleekxmpp/plugins/xep_0009/stanza',
'sleekxmpp/plugins/xep_0030',
'sleekxmpp/plugins/xep_0030/stanza',
+ 'sleekxmpp/plugins/xep_0047',
'sleekxmpp/plugins/xep_0050',
'sleekxmpp/plugins/xep_0059',
'sleekxmpp/plugins/xep_0060',
'sleekxmpp/plugins/xep_0060/stanza',
'sleekxmpp/plugins/xep_0066',
+ 'sleekxmpp/plugins/xep_0077',
'sleekxmpp/plugins/xep_0078',
+ 'sleekxmpp/plugins/xep_0080',
'sleekxmpp/plugins/xep_0085',
'sleekxmpp/plugins/xep_0086',
'sleekxmpp/plugins/xep_0092',
+ 'sleekxmpp/plugins/xep_0107',
+ 'sleekxmpp/plugins/xep_0108',
+ 'sleekxmpp/plugins/xep_0115',
+ 'sleekxmpp/plugins/xep_0118',
'sleekxmpp/plugins/xep_0128',
+ 'sleekxmpp/plugins/xep_0172',
+ 'sleekxmpp/plugins/xep_0184',
+ 'sleekxmpp/plugins/xep_0198',
'sleekxmpp/plugins/xep_0199',
'sleekxmpp/plugins/xep_0202',
'sleekxmpp/plugins/xep_0203',
@@ -79,6 +90,7 @@ packages = [ 'sleekxmpp',
'sleekxmpp/features/feature_starttls',
'sleekxmpp/features/feature_bind',
'sleekxmpp/features/feature_session',
+ 'sleekxmpp/features/feature_rosterver',
'sleekxmpp/thirdparty',
'sleekxmpp/thirdparty/suelta',
'sleekxmpp/thirdparty/suelta/mechanisms',
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
index 11e787ad..dc1f6b94 100644
--- a/sleekxmpp/basexmpp.py
+++ b/sleekxmpp/basexmpp.py
@@ -15,22 +15,24 @@
from __future__ import with_statement, unicode_literals
import sys
-import copy
import logging
import sleekxmpp
-from sleekxmpp import plugins, roster
+from sleekxmpp import plugins, features, roster
from sleekxmpp.exceptions import IqError, IqTimeout
-from sleekxmpp.stanza import Message, Presence, Iq, Error, StreamError
+from sleekxmpp.stanza import Message, Presence, Iq, StreamError
from sleekxmpp.stanza.roster import Roster
from sleekxmpp.stanza.nick import Nick
from sleekxmpp.stanza.htmlim import HTMLIM
-from sleekxmpp.xmlstream import XMLStream, JID, tostring
+from sleekxmpp.xmlstream import XMLStream, JID
from sleekxmpp.xmlstream import ET, register_stanza_plugin
-from sleekxmpp.xmlstream.matcher import *
-from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.xmlstream.handler import Callback
+
+from sleekxmpp.features import *
+from sleekxmpp.plugins import PluginManager, register_plugin, load_plugin
log = logging.getLogger(__name__)
@@ -67,7 +69,7 @@ class BaseXMPP(XMLStream):
self.boundjid = JID(jid)
#: A dictionary mapping plugin names to plugins.
- self.plugin = {}
+ self.plugin = PluginManager(self)
#: Configuration options for whitelisted plugins.
#: If a plugin is registered without any configuration,
@@ -186,9 +188,18 @@ class BaseXMPP(XMLStream):
- The send queue processor
- The scheduler
"""
+ if 'xep_0115' in self.plugin:
+ name = 'xep_0115'
+ if not hasattr(self.plugin[name], 'post_inited'):
+ if hasattr(self.plugin[name], 'post_init'):
+ self.plugin[name].post_init()
+ self.plugin[name].post_inited = True
+
for name in self.plugin:
- if not self.plugin[name].post_inited:
- self.plugin[name].post_init()
+ if not hasattr(self.plugin[name], 'post_inited'):
+ if hasattr(self.plugin[name], 'post_init'):
+ self.plugin[name].post_init()
+ self.plugin[name].post_inited = True
return XMLStream.process(self, *args, **kwargs)
def register_plugin(self, plugin, pconfig={}, module=None):
@@ -201,42 +212,14 @@ class BaseXMPP(XMLStream):
:param module: Optional refence to the module containing the plugin
class if using custom plugins.
"""
- try:
- # Import the given module that contains the plugin.
- if not module:
- try:
- module = sleekxmpp.plugins
- module = __import__(
- str("%s.%s" % (module.__name__, plugin)),
- globals(), locals(), [str(plugin)])
- except ImportError:
- module = sleekxmpp.features
- module = __import__(
- str("%s.%s" % (module.__name__, plugin)),
- globals(), locals(), [str(plugin)])
- if isinstance(module, str):
- # We probably want to load a module from outside
- # the sleekxmpp package, so leave out the globals().
- module = __import__(module, fromlist=[plugin])
-
- # Use the global plugin config cache, if applicable
- if not pconfig:
- pconfig = self.plugin_config.get(plugin, {})
-
- # Load the plugin class from the module.
- self.plugin[plugin] = getattr(module, plugin)(self, pconfig)
-
- # Let XEP/RFC implementing plugins have some extra logging info.
- spec = '(CUSTOM) %s'
- if self.plugin[plugin].xep:
- spec = "(XEP-%s) " % self.plugin[plugin].xep
- elif self.plugin[plugin].rfc:
- spec = "(RFC-%s) " % self.plugin[plugin].rfc
-
- desc = (spec, self.plugin[plugin].description)
- log.debug("Loaded Plugin %s %s" % desc)
- except:
- log.exception("Unable to load plugin: %s", plugin)
+
+ # Use the global plugin config cache, if applicable
+ if not pconfig:
+ pconfig = self.plugin_config.get(plugin, {})
+
+ if not self.plugin.registered(plugin):
+ load_plugin(plugin, module)
+ self.plugin.enable(plugin, pconfig)
def register_plugins(self):
"""Register and initialize all built-in plugins.
@@ -253,15 +236,10 @@ class BaseXMPP(XMLStream):
for plugin in plugin_list:
if plugin in plugins.__all__:
- self.register_plugin(plugin,
- self.plugin_config.get(plugin, {}))
+ self.register_plugin(plugin)
else:
raise NameError("Plugin %s not in plugins.__all__." % plugin)
- # Resolve plugin inter-dependencies.
- for plugin in self.plugin:
- self.plugin[plugin].post_init()
-
def __getitem__(self, key):
"""Return a plugin given its name, if it has been registered."""
if key in self.plugin:
@@ -675,11 +653,15 @@ class BaseXMPP(XMLStream):
def _handle_available(self, presence):
pto = presence['to'].bare
+ if not pto:
+ pto = self.boundjid.bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_available(presence)
def _handle_unavailable(self, presence):
pto = presence['to'].bare
+ if not pto:
+ pto = self.boundjid.bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_unavailable(presence)
@@ -763,6 +745,11 @@ class BaseXMPP(XMLStream):
iq = exception.iq
log.error('Request timed out: %s', iq)
log.warning('You should catch IqTimeout exceptions')
+ elif isinstance(exception, SyntaxError):
+ # Hide stream parsing errors that occur when the
+ # stream is disconnected (they've been handled, we
+ # don't need to make a mess in the logs).
+ pass
else:
log.exception(exception)
diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py
index 20012b5f..590192db 100644
--- a/sleekxmpp/clientxmpp.py
+++ b/sleekxmpp/clientxmpp.py
@@ -15,22 +15,13 @@
from __future__ import absolute_import, unicode_literals
import logging
-import base64
-import sys
-import hashlib
-import random
-import threading
-
-import sleekxmpp
-from sleekxmpp import plugins
-from sleekxmpp import stanza
-from sleekxmpp import features
+
+from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.basexmpp import BaseXMPP
-from sleekxmpp.stanza import *
-from sleekxmpp.xmlstream import XMLStream, RestartStream
-from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
-from sleekxmpp.xmlstream.matcher import *
-from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.xmlstream import XMLStream
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.xmlstream.handler import Callback
# Flag indicating if DNS SRV records are available for use.
try:
@@ -74,12 +65,15 @@ class ClientXMPP(BaseXMPP):
BaseXMPP.__init__(self, jid, 'jabber:client')
self.set_jid(jid)
- self.password = password
self.escape_quotes = escape_quotes
self.plugin_config = plugin_config
self.plugin_whitelist = plugin_whitelist
self.default_port = 5222
+ self.credentials = {}
+
+ self.password = password
+
self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
self.boundjid.host,
"xmlns:stream='%s'" % self.stream_ns,
@@ -97,6 +91,7 @@ class ClientXMPP(BaseXMPP):
self.bindfail = False
self.add_event_handler('connected', self._handle_connected)
+ self.add_event_handler('session_bind', self._handle_session_bind)
self.register_stanza(StreamFeatures)
@@ -117,6 +112,15 @@ class ClientXMPP(BaseXMPP):
self.register_plugin('feature_session')
self.register_plugin('feature_mechanisms',
pconfig={'use_mech': sasl_mech} if sasl_mech else None)
+ self.register_plugin('feature_rosterver')
+
+ @property
+ def password(self):
+ return self.credentials.get('password', '')
+
+ @password.setter
+ def password(self, value):
+ self.credentials['password'] = value
def connect(self, address=tuple(), reattempt=True,
use_tls=True, use_ssl=False):
@@ -154,8 +158,10 @@ class ClientXMPP(BaseXMPP):
try:
record = "_xmpp-client._tcp.%s" % domain
answers = []
+ log.debug("Querying SRV records for %s" % domain)
for answer in dns.resolver.query(record, dns.rdatatype.SRV):
address = (answer.target.to_text()[:-1], answer.port)
+ log.debug("Found SRV record: %s", address)
answers.append((address, answer.priority, answer.weight))
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
log.warning("No SRV records for %s", domain)
@@ -167,7 +173,7 @@ class ClientXMPP(BaseXMPP):
return answers
else:
log.warning("dnspython is not installed -- " + \
- "relying on OS A record resolution")
+ "relying on OS A/AAAA record resolution")
return [((domain, port), 0, 0)]
def register_feature(self, name, handler, restart=False, order=5000):
@@ -236,10 +242,17 @@ class ClientXMPP(BaseXMPP):
iq = self.Iq()
iq['type'] = 'get'
iq.enable('roster')
+ if 'rosterver' in self.features:
+ iq['roster']['ver'] = self.client_roster.version
+
+ if not block and callback is None:
+ callback = lambda resp: self._handle_roster(resp, request=True)
+
response = iq.send(block, timeout, callback)
- if callback is None:
- return self._handle_roster(response, request=True)
+ if block:
+ self._handle_roster(response, request=True)
+ return response
def _handle_connected(self, event=None):
#TODO: Use stream state here
@@ -270,15 +283,22 @@ class ClientXMPP(BaseXMPP):
to a request for the roster, and not an
empty acknowledgement from the server.
"""
+ if iq['from'].bare and iq['from'].bare != self.boundjid.bare:
+ raise XMPPError(condition='service-unavailable')
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
+ roster = self.client_roster
+ if iq['roster']['ver']:
+ roster.version = iq['roster']['ver']
for jid in iq['roster']['items']:
item = iq['roster']['items'][jid]
- roster = self.roster[iq['to'].bare]
roster[jid]['name'] = item['name']
roster[jid]['groups'] = item['groups']
roster[jid]['from'] = item['subscription'] in ['from', 'both']
roster[jid]['to'] = item['subscription'] in ['to', 'both']
roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
+
+ roster[jid].save(remove=(item['subscription'] == 'remove'))
+
self.event('roster_received', iq)
self.event("roster_update", iq)
@@ -286,7 +306,14 @@ class ClientXMPP(BaseXMPP):
iq.reply()
iq.enable('roster')
iq.send()
- return True
+
+ def _handle_session_bind(self, jid):
+ """Set the client roster to the JID set by the server.
+
+ :param :class:`sleekxmpp.xmlstream.jid.JID` jid: The bound JID as
+ dictated by the server. The same as :attr:`boundjid`.
+ """
+ self.client_roster = self.roster[jid]
# To comply with PEP8, method names now use underscores.
diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py
index 5b16c5ef..df23c2f6 100644
--- a/sleekxmpp/componentxmpp.py
+++ b/sleekxmpp/componentxmpp.py
@@ -15,17 +15,14 @@
from __future__ import absolute_import
import logging
-import base64
import sys
import hashlib
-from sleekxmpp import plugins
-from sleekxmpp import stanza
from sleekxmpp.basexmpp import BaseXMPP
-from sleekxmpp.xmlstream import XMLStream, RestartStream
-from sleekxmpp.xmlstream import StanzaBase, ET
-from sleekxmpp.xmlstream.matcher import *
-from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.xmlstream import XMLStream
+from sleekxmpp.xmlstream import ET
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.xmlstream.handler import Callback
log = logging.getLogger(__name__)
diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py
index 5bfe173d..c63d72bf 100644
--- a/sleekxmpp/features/__init__.py
+++ b/sleekxmpp/features/__init__.py
@@ -6,4 +6,10 @@
See the file LICENSE for copying permission.
"""
-__all__ = ['feature_starttls', 'feature_mechanisms', 'feature_bind']
+__all__ = [
+ 'feature_starttls',
+ 'feature_mechanisms',
+ 'feature_bind',
+ 'feature_session',
+ 'feature_rosterver'
+]
diff --git a/sleekxmpp/features/feature_bind/__init__.py b/sleekxmpp/features/feature_bind/__init__.py
index aa854f87..9e0831dd 100644
--- a/sleekxmpp/features/feature_bind/__init__.py
+++ b/sleekxmpp/features/feature_bind/__init__.py
@@ -6,5 +6,14 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.features.feature_bind.bind import feature_bind
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.features.feature_bind.bind import FeatureBind
from sleekxmpp.features.feature_bind.stanza import Bind
+
+
+register_plugin(FeatureBind)
+
+
+# Retain some backwards compatibility
+feature_bind = FeatureBind
diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py
index d3b2b737..b828e26f 100644
--- a/sleekxmpp/features/feature_bind/bind.py
+++ b/sleekxmpp/features/feature_bind/bind.py
@@ -11,22 +11,20 @@ import logging
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.features.feature_bind import stanza
from sleekxmpp.xmlstream import register_stanza_plugin
-from sleekxmpp.xmlstream.matcher import *
-from sleekxmpp.xmlstream.handler import *
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin, register_plugin
log = logging.getLogger(__name__)
-class feature_bind(base_plugin):
+class FeatureBind(BasePlugin):
- def plugin_init(self):
- self.name = 'Bind Resource'
- self.rfc = '6120'
- self.description = 'Resource Binding Stream Feature'
- self.stanza = stanza
+ name = 'feature_bind'
+ description = 'RFC 6120: Stream Feature: Resource Binding'
+ dependencies = set()
+ stanza = stanza
+ def plugin_init(self):
self.xmpp.register_feature('bind',
self._handle_bind_resource,
restart=False,
@@ -52,6 +50,7 @@ class feature_bind(base_plugin):
self.xmpp.set_jid(response['bind']['jid'])
self.xmpp.bound = True
+ self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True)
self.xmpp.features.add('bind')
diff --git a/sleekxmpp/features/feature_bind/stanza.py b/sleekxmpp/features/feature_bind/stanza.py
index 2c1484e0..8ce7536f 100644
--- a/sleekxmpp/features/feature_bind/stanza.py
+++ b/sleekxmpp/features/feature_bind/stanza.py
@@ -6,8 +6,7 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.stanza import Iq, StreamFeatures
-from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+from sleekxmpp.xmlstream import ElementBase
class Bind(ElementBase):
diff --git a/sleekxmpp/features/feature_mechanisms/__init__.py b/sleekxmpp/features/feature_mechanisms/__init__.py
index 5379ef4e..9f7611ed 100644
--- a/sleekxmpp/features/feature_mechanisms/__init__.py
+++ b/sleekxmpp/features/feature_mechanisms/__init__.py
@@ -6,8 +6,17 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.features.feature_mechanisms.mechanisms import feature_mechanisms
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.features.feature_mechanisms.mechanisms import FeatureMechanisms
from sleekxmpp.features.feature_mechanisms.stanza import Mechanisms
from sleekxmpp.features.feature_mechanisms.stanza import Auth
from sleekxmpp.features.feature_mechanisms.stanza import Success
from sleekxmpp.features.feature_mechanisms.stanza import Failure
+
+
+register_plugin(FeatureMechanisms)
+
+
+# Retain some backwards compatibility
+feature_mechanisms = FeatureMechanisms
diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py
index 2b8321c2..6f01cb14 100644
--- a/sleekxmpp/features/feature_mechanisms/mechanisms.py
+++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py
@@ -9,36 +9,47 @@
import logging
from sleekxmpp.thirdparty import suelta
+from sleekxmpp.thirdparty.suelta.exceptions import SASLCancelled, SASLError
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
-from sleekxmpp.xmlstream.matcher import *
-from sleekxmpp.xmlstream.handler import *
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.features.feature_mechanisms import stanza
log = logging.getLogger(__name__)
-class feature_mechanisms(base_plugin):
+class FeatureMechanisms(BasePlugin):
- def plugin_init(self):
- self.name = 'SASL Mechanisms'
- self.rfc = '6120'
- self.description = "SASL Stream Feature"
- self.stanza = stanza
+ name = 'feature_mechanisms'
+ description = 'RFC 6120: Stream Feature: SASL'
+ dependencies = set()
+ stanza = stanza
+ def plugin_init(self):
self.use_mech = self.config.get('use_mech', None)
+ if not self.use_mech and not self.xmpp.boundjid.user:
+ self.use_mech = 'ANONYMOUS'
+
def tls_active():
return 'starttls' in self.xmpp.features
def basic_callback(mech, values):
- if 'username' in values:
- values['username'] = self.xmpp.boundjid.user
- if 'password' in values:
- values['password'] = self.xmpp.password
+ creds = self.xmpp.credentials
+ for value in values:
+ if value == 'username':
+ values['username'] = self.xmpp.boundjid.user
+ elif value == 'password':
+ values['password'] = creds['password']
+ elif value == 'email':
+ jid = self.xmpp.boundjid.bare
+ values['email'] = creds.get('email', jid)
+ elif value in creds:
+ values[value] = creds[value]
mech.fulfill(values)
sasl_callback = self.config.get('sasl_callback', None)
@@ -53,6 +64,9 @@ class feature_mechanisms(base_plugin):
tls_active=tls_active,
mech=self.use_mech)
+ self.mech_list = set()
+ self.attempted_mechs = set()
+
register_stanza_plugin(StreamFeatures, stanza.Mechanisms)
self.xmpp.register_stanza(stanza.Success)
@@ -60,19 +74,18 @@ class feature_mechanisms(base_plugin):
self.xmpp.register_stanza(stanza.Auth)
self.xmpp.register_stanza(stanza.Challenge)
self.xmpp.register_stanza(stanza.Response)
+ self.xmpp.register_stanza(stanza.Abort)
self.xmpp.register_handler(
Callback('SASL Success',
MatchXPath(stanza.Success.tag_name()),
self._handle_success,
- instream=True,
- once=True))
+ instream=True))
self.xmpp.register_handler(
Callback('SASL Failure',
MatchXPath(stanza.Failure.tag_name()),
self._handle_fail,
- instream=True,
- once=True))
+ instream=True))
self.xmpp.register_handler(
Callback('SASL Challenge',
MatchXPath(stanza.Challenge.tag_name()),
@@ -95,14 +108,29 @@ class feature_mechanisms(base_plugin):
# server has incorrectly offered it again.
return False
- mech_list = features['mechanisms']
+ if not self.use_mech:
+ self.mech_list = set(features['mechanisms'])
+ else:
+ self.mech_list = set([self.use_mech])
+ return self._send_auth()
+
+ def _send_auth(self):
+ mech_list = self.mech_list - self.attempted_mechs
self.mech = self.sasl.choose_mechanism(mech_list)
- if self.mech is not None:
+ if mech_list and self.mech is not None:
resp = stanza.Auth(self.xmpp)
resp['mechanism'] = self.mech.name
- resp['value'] = self.mech.process()
- resp.send(now=True)
+ try:
+ resp['value'] = self.mech.process()
+ except SASLCancelled:
+ self.attempted_mechs.add(self.mech.name)
+ self._send_auth()
+ except SASLError:
+ self.attempted_mechs.add(self.mech.name)
+ self._send_auth()
+ else:
+ resp.send(now=True)
else:
log.error("No appropriate login method.")
self.xmpp.event("no_auth", direct=True)
@@ -112,18 +140,26 @@ class feature_mechanisms(base_plugin):
def _handle_challenge(self, stanza):
"""SASL challenge received. Process and send response."""
resp = self.stanza.Response(self.xmpp)
- resp['value'] = self.mech.process(stanza['value'])
- resp.send(now=True)
+ try:
+ resp['value'] = self.mech.process(stanza['value'])
+ except SASLCancelled:
+ self.stanza.Abort(self.xmpp).send()
+ except SASLError:
+ self.stanza.Abort(self.xmpp).send()
+ else:
+ resp.send(now=True)
def _handle_success(self, stanza):
"""SASL authentication succeeded. Restart the stream."""
+ self.attempted_mechs = set()
self.xmpp.authenticated = True
self.xmpp.features.add('mechanisms')
raise RestartStream()
def _handle_fail(self, stanza):
"""SASL authentication failed. Disconnect and shutdown."""
+ self.attempted_mechs.add(self.mech.name)
log.info("Authentication failed: %s", stanza['condition'])
self.xmpp.event("failed_auth", stanza, direct=True)
- self.xmpp.disconnect()
+ self._send_auth()
return True
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/__init__.py b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py
index 8b80f358..38991d89 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/__init__.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py
@@ -13,3 +13,4 @@ from sleekxmpp.features.feature_mechanisms.stanza.success import Success
from sleekxmpp.features.feature_mechanisms.stanza.failure import Failure
from sleekxmpp.features.feature_mechanisms.stanza.challenge import Challenge
from sleekxmpp.features.feature_mechanisms.stanza.response import Response
+from sleekxmpp.features.feature_mechanisms.stanza.abort import Abort
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/abort.py b/sleekxmpp/features/feature_mechanisms/stanza/abort.py
new file mode 100644
index 00000000..aaca348d
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/abort.py
@@ -0,0 +1,24 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import StanzaBase
+
+
+class Abort(StanzaBase):
+
+ """
+ """
+
+ name = 'abort'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set()
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/auth.py b/sleekxmpp/features/feature_mechanisms/stanza/auth.py
index e069b57f..69769507 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/auth.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py
@@ -10,9 +10,7 @@ import base64
from sleekxmpp.thirdparty.suelta.util import bytes
-from sleekxmpp.stanza import StreamFeatures
-from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
-from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream import StanzaBase
class Auth(StanzaBase):
@@ -25,15 +23,28 @@ class Auth(StanzaBase):
interfaces = set(('mechanism', 'value'))
plugin_attrib = name
+ #: Some SASL mechs require sending values as is,
+ #: without converting base64.
+ plain_mechs = set(['X-MESSENGER-OAUTH2'])
+
def setup(self, xml):
StanzaBase.setup(self, xml)
self.xml.tag = self.tag_name()
def get_value(self):
- return base64.b64decode(bytes(self.xml.text))
+ if not self['mechanism'] in self.plain_mechs:
+ return base64.b64decode(bytes(self.xml.text))
+ else:
+ return self.xml.text
def set_value(self, values):
- self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ if not self['mechanism'] in self.plain_mechs:
+ if values:
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ else:
+ self.xml.text = '='
+ else:
+ self.xml.text = bytes(values).decode('utf-8')
def del_value(self):
self.xml.text = ''
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/challenge.py b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py
index 82af869f..85d65403 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/challenge.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py
@@ -10,9 +10,7 @@ import base64
from sleekxmpp.thirdparty.suelta.util import bytes
-from sleekxmpp.stanza import StreamFeatures
-from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
-from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream import StanzaBase
class Challenge(StanzaBase):
@@ -33,7 +31,10 @@ class Challenge(StanzaBase):
return base64.b64decode(bytes(self.xml.text))
def set_value(self, values):
- self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ if values:
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ else:
+ self.xml.text = '='
def del_value(self):
self.xml.text = ''
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/failure.py b/sleekxmpp/features/feature_mechanisms/stanza/failure.py
index 027cc5af..5dd0de56 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/failure.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/failure.py
@@ -6,9 +6,7 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.stanza import StreamFeatures
-from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
-from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream import StanzaBase, ET
class Failure(StanzaBase):
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py b/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py
index c09cafbd..bbd56813 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py
@@ -6,9 +6,7 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.stanza import StreamFeatures
-from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
-from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream import ElementBase, ET
class Mechanisms(ElementBase):
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/response.py b/sleekxmpp/features/feature_mechanisms/stanza/response.py
index 45bb8207..78636c9e 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/response.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/response.py
@@ -10,9 +10,7 @@ import base64
from sleekxmpp.thirdparty.suelta.util import bytes
-from sleekxmpp.stanza import StreamFeatures
-from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
-from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream import StanzaBase
class Response(StanzaBase):
@@ -33,7 +31,10 @@ class Response(StanzaBase):
return base64.b64decode(bytes(self.xml.text))
def set_value(self, values):
- self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ if values:
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ else:
+ self.xml.text = '='
def del_value(self):
self.xml.text = ''
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/success.py b/sleekxmpp/features/feature_mechanisms/stanza/success.py
index 028e28a3..7a5a73f2 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/success.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/success.py
@@ -6,9 +6,7 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.stanza import StreamFeatures
-from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
-from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream import StanzaBase
class Success(StanzaBase):
diff --git a/sleekxmpp/features/feature_rosterver/__init__.py b/sleekxmpp/features/feature_rosterver/__init__.py
new file mode 100644
index 00000000..33bbf416
--- /dev/null
+++ b/sleekxmpp/features/feature_rosterver/__init__.py
@@ -0,0 +1,19 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.features.feature_rosterver.rosterver import FeatureRosterVer
+from sleekxmpp.features.feature_rosterver.stanza import RosterVer
+
+
+register_plugin(FeatureRosterVer)
+
+
+# Retain some backwards compatibility
+feature_rosterver = FeatureRosterVer
diff --git a/sleekxmpp/features/feature_rosterver/rosterver.py b/sleekxmpp/features/feature_rosterver/rosterver.py
new file mode 100644
index 00000000..9e0bb8e8
--- /dev/null
+++ b/sleekxmpp/features/feature_rosterver/rosterver.py
@@ -0,0 +1,42 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.features.feature_rosterver import stanza
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.base import BasePlugin
+
+
+log = logging.getLogger(__name__)
+
+
+class FeatureRosterVer(BasePlugin):
+
+ name = 'feature_rosterver'
+ description = 'RFC 6121: Stream Feature: Roster Versioning'
+ dependences = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_feature('rosterver',
+ self._handle_rosterver,
+ restart=False,
+ order=9000)
+
+ register_stanza_plugin(StreamFeatures, stanza.RosterVer)
+
+ def _handle_rosterver(self, features):
+ """Enable using roster versioning.
+
+ Arguments:
+ features -- The stream features stanza.
+ """
+ log.debug("Enabling roster versioning.")
+ self.xmpp.features.add('rosterver')
diff --git a/sleekxmpp/features/feature_rosterver/stanza.py b/sleekxmpp/features/feature_rosterver/stanza.py
new file mode 100644
index 00000000..025872fa
--- /dev/null
+++ b/sleekxmpp/features/feature_rosterver/stanza.py
@@ -0,0 +1,17 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase
+
+
+class RosterVer(ElementBase):
+
+ name = 'ver'
+ namespace = 'urn:xmpp:features:rosterver'
+ interfaces = set()
+ plugin_attrib = 'rosterver'
diff --git a/sleekxmpp/features/feature_session/__init__.py b/sleekxmpp/features/feature_session/__init__.py
index 3c84baed..28bb3f77 100644
--- a/sleekxmpp/features/feature_session/__init__.py
+++ b/sleekxmpp/features/feature_session/__init__.py
@@ -6,5 +6,14 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.features.feature_session.session import feature_session
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.features.feature_session.session import FeatureSession
from sleekxmpp.features.feature_session.stanza import Session
+
+
+register_plugin(FeatureSession)
+
+
+# Retain some backwards compatibility
+feature_session = FeatureSession
diff --git a/sleekxmpp/features/feature_session/session.py b/sleekxmpp/features/feature_session/session.py
index 0daec5da..c799a763 100644
--- a/sleekxmpp/features/feature_session/session.py
+++ b/sleekxmpp/features/feature_session/session.py
@@ -10,9 +10,7 @@ import logging
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.xmlstream import register_stanza_plugin
-from sleekxmpp.xmlstream.matcher import *
-from sleekxmpp.xmlstream.handler import *
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.features.feature_session import stanza
@@ -20,14 +18,14 @@ from sleekxmpp.features.feature_session import stanza
log = logging.getLogger(__name__)
-class feature_session(base_plugin):
+class FeatureSession(BasePlugin):
- def plugin_init(self):
- self.name = 'Start Session'
- self.rfc = '3920'
- self.description = 'Start Session Stream Feature'
- self.stanza = stanza
+ name = 'feature_session'
+ description = 'RFC 3920: Stream Feature: Start Session'
+ dependencies = set()
+ stanza = stanza
+ def plugin_init(self):
self.xmpp.register_feature('session',
self._handle_start_session,
restart=False,
@@ -46,7 +44,7 @@ class feature_session(base_plugin):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq.enable('session')
- response = iq.send(now=True)
+ iq.send(now=True)
self.xmpp.features.add('session')
diff --git a/sleekxmpp/features/feature_session/stanza.py b/sleekxmpp/features/feature_session/stanza.py
index 40ea583d..94e949ee 100644
--- a/sleekxmpp/features/feature_session/stanza.py
+++ b/sleekxmpp/features/feature_session/stanza.py
@@ -6,8 +6,7 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.stanza import Iq, StreamFeatures
-from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+from sleekxmpp.xmlstream import ElementBase
class Session(ElementBase):
diff --git a/sleekxmpp/features/feature_starttls/__init__.py b/sleekxmpp/features/feature_starttls/__init__.py
index 4ae89433..68697ce5 100644
--- a/sleekxmpp/features/feature_starttls/__init__.py
+++ b/sleekxmpp/features/feature_starttls/__init__.py
@@ -6,5 +6,14 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.features.feature_starttls.starttls import feature_starttls
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.features.feature_starttls.starttls import FeatureSTARTTLS
from sleekxmpp.features.feature_starttls.stanza import *
+
+
+register_plugin(FeatureSTARTTLS)
+
+
+# Retain some backwards compatibility
+feature_starttls = FeatureSTARTTLS
diff --git a/sleekxmpp/features/feature_starttls/stanza.py b/sleekxmpp/features/feature_starttls/stanza.py
index 8b09ad94..b968e134 100644
--- a/sleekxmpp/features/feature_starttls/stanza.py
+++ b/sleekxmpp/features/feature_starttls/stanza.py
@@ -6,9 +6,7 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import StanzaBase, ElementBase
-from sleekxmpp.xmlstream import register_stanza_plugin
class STARTTLS(ElementBase):
diff --git a/sleekxmpp/features/feature_starttls/starttls.py b/sleekxmpp/features/feature_starttls/starttls.py
index 4e2b6621..212b9da5 100644
--- a/sleekxmpp/features/feature_starttls/starttls.py
+++ b/sleekxmpp/features/feature_starttls/starttls.py
@@ -10,23 +10,23 @@ import logging
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
-from sleekxmpp.xmlstream.matcher import *
-from sleekxmpp.xmlstream.handler import *
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.features.feature_starttls import stanza
log = logging.getLogger(__name__)
-class feature_starttls(base_plugin):
+class FeatureSTARTTLS(BasePlugin):
- def plugin_init(self):
- self.name = "STARTTLS"
- self.rfc = '6120'
- self.description = "STARTTLS Stream Feature"
- self.stanza = stanza
+ name = 'feature_starttls'
+ description = 'RFC 6120: Stream Feature: STARTTLS'
+ dependencies = set()
+ stanza = stanza
+ def plugin_init(self):
self.xmpp.register_handler(
Callback('STARTTLS Proceed',
MatchXPath(stanza.Proceed.tag_name()),
diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py
index c0b1121b..c374f27b 100644
--- a/sleekxmpp/plugins/__init__.py
+++ b/sleekxmpp/plugins/__init__.py
@@ -5,9 +5,46 @@
See the file LICENSE for copying permission.
"""
-__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
- 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082',
- 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199',
- 'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify']
-# Don't automatically load xep_0078
+from sleekxmpp.plugins.base import PluginManager, PluginNotFound, BasePlugin
+from sleekxmpp.plugins.base import register_plugin, load_plugin
+
+
+__all__ = [
+ # Non-standard
+ 'gmail_notify', # Gmail searching and notifications
+
+ # XEPS
+ 'xep_0004', # Data Forms
+ 'xep_0009', # Jabber-RPC
+ 'xep_0012', # Last Activity
+ 'xep_0030', # Service Discovery
+ 'xep_0033', # Extended Stanza Addresses
+ 'xep_0045', # Multi-User Chat (Client)
+ 'xep_0047', # In-Band Bytestreams
+ 'xep_0050', # Ad-hoc Commands
+ 'xep_0059', # Result Set Management
+ 'xep_0060', # Pubsub (Client)
+ 'xep_0066', # Out of Band Data
+ 'xep_0077', # In-Band Registration
+# 'xep_0078', # Non-SASL auth. Don't automatically load
+ 'xep_0080', # User Location
+ 'xep_0082', # XMPP Date and Time Profiles
+ 'xep_0085', # Chat State Notifications
+ 'xep_0086', # Legacy Error Codes
+ 'xep_0092', # Software Version
+ 'xep_0107', # User Mood
+ 'xep_0108', # User Activity
+ 'xep_0115', # Entity Capabilities
+ 'xep_0118', # User Tune
+ 'xep_0128', # Extended Service Discovery
+ 'xep_0163', # Personal Eventing Protocol
+ 'xep_0172', # User Nickname
+ 'xep_0184', # Message Receipts
+ 'xep_0198', # Stream Management
+ 'xep_0199', # Ping
+ 'xep_0202', # Entity Time
+ 'xep_0203', # Delayed Delivery
+ 'xep_0224', # Attention
+ 'xep_0249', # Direct MUC Invitations
+]
diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py
index 561421d8..f08023ba 100644
--- a/sleekxmpp/plugins/base.py
+++ b/sleekxmpp/plugins/base.py
@@ -1,91 +1,293 @@
+# -*- encoding: utf-8 -*-
+
"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2010 Nathanael C. Fritz
- This file is part of SleekXMPP.
+ sleekxmpp.plugins.base
+ ~~~~~~~~~~~~~~~~~~~~~~
+
+ This module provides XMPP functionality that
+ is specific to client connections.
+
+ Part of SleekXMPP: The Sleek XMPP Library
- See the file LICENSE for copying permission.
+ :copyright: (c) 2012 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
"""
+import sys
+import logging
+import threading
+
+
+log = logging.getLogger(__name__)
+
+
+#: Associate short string names of plugins with implementations. The
+#: plugin names are based on the spec used by the plugin, such as
+#: `'xep_0030'` for a plugin that implements XEP-0030.
+PLUGIN_REGISTRY = {}
+
+#: In order to do cascading plugin disabling, reverse dependencies
+#: must be tracked.
+PLUGIN_DEPENDENTS = {}
+
+#: Only allow one thread to manipulate the plugin registry at a time.
+REGISTRY_LOCK = threading.RLock()
-class base_plugin(object):
+class PluginNotFound(Exception):
+ """Raised if an unknown plugin is accessed."""
+
+
+def register_plugin(impl, name=None):
+ """Add a new plugin implementation to the registry.
+
+ :param class impl: The plugin class.
+
+ The implementation class must provide a :attr:`~BasePlugin.name`
+ value that will be used as a short name for enabling and disabling
+ the plugin. The name should be based on the specification used by
+ the plugin. For example, a plugin implementing XEP-0030 would be
+ named `'xep_0030'`.
"""
- The base_plugin class serves as a base for user created plugins
- that provide support for existing or experimental XEPS.
-
- Each plugin has a dictionary for configuration options, as well
- as a name and description.
-
- The lifecycle of a plugin is:
- 1. The plugin is instantiated during registration.
- 2. Once the XML stream begins processing, the method
- plugin_init() is called (if the plugin is configured
- as enabled with {'enable': True}).
- 3. After all plugins have been initialized, the
- method post_init() is called.
-
- Recommended event handlers:
- session_start -- Plugins which require the use of the current
- bound JID SHOULD wait for the session_start
- event to perform any initialization (or
- resetting). This is a transitive recommendation,
- plugins that use other plugins which use the
- bound JID should also wait for session_start
- before making such calls.
- session_end -- If the plugin keeps any per-session state,
- such as joined MUC rooms, such state SHOULD
- be cleared when the session_end event is raised.
-
- Attributes:
- xep -- The XEP number the plugin implements, if any.
- description -- A short description of the plugin, typically
- the long name of the implemented XEP.
- xmpp -- The main SleekXMPP instance.
- config -- A dictionary of custom configuration values.
- The value 'enable' is special and controls
- whether or not the plugin is initialized
- after registration.
- post_initted -- Executed after all plugins have been initialized
- to handle any cross-plugin interactions, such as
- registering service discovery items.
- enable -- Indicates that the plugin is enabled for use and
- will be initialized after registration.
-
- Methods:
- plugin_init -- Initialize the plugin state.
- post_init -- Handle any cross-plugin concerns.
+ if name is None:
+ name = impl.name
+ with REGISTRY_LOCK:
+ PLUGIN_REGISTRY[name] = impl
+ if name not in PLUGIN_DEPENDENTS:
+ PLUGIN_DEPENDENTS[name] = set()
+ for dep in impl.dependencies:
+ if dep not in PLUGIN_DEPENDENTS:
+ PLUGIN_DEPENDENTS[dep] = set()
+ PLUGIN_DEPENDENTS[dep].add(name)
+
+
+def load_plugin(name, module=None):
+ """Find and import a plugin module so that it can be registered.
+
+ This function is called to import plugins that have selected for
+ enabling, but no matching registered plugin has been found.
+
+ :param str name: The name of the plugin. It is expected that
+ plugins are in packages matching their name,
+ even though the plugin class name does not
+ have to match.
+ :param str module: The name of the base module to search
+ for the plugin.
"""
+ try:
+ if not module:
+ try:
+ module = 'sleekxmpp.plugins.%s' % name
+ __import__(module)
+ mod = sys.modules[module]
+ except:
+ module = 'sleekxmpp.features.%s' % name
+ __import__(module)
+ mod = sys.modules[module]
+ else:
+ __import__(module)
+ mod = sys.modules[module]
+ # Add older style plugins to the registry.
+ if hasattr(mod, name):
+ plugin = getattr(mod, name)
+ if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'):
+ plugin.name = name
+ # Mark the plugin as an older style plugin so
+ # we can work around dependency issues.
+ plugin.old_style = True
+ register_plugin(plugin, name)
+ except:
+ log.exception("Unable to load plugin: %s", name)
+
+
+class PluginManager(object):
def __init__(self, xmpp, config=None):
+ #: We will track all enabled plugins in a set so that we
+ #: can enable plugins in batches and pull in dependencies
+ #: without problems.
+ self._enabled = set()
+
+ #: Maintain references to active plugins.
+ self._plugins = {}
+
+ self._plugin_lock = threading.RLock()
+
+ #: Globally set default plugin configuration. This will
+ #: be used for plugins that are auto-enabled through
+ #: dependency loading.
+ self.config = config if config else {}
+
+ self.xmpp = xmpp
+
+ def register(self, plugin, enable=True):
+ """Register a new plugin, and optionally enable it.
+
+ :param class plugin: The implementation class of the plugin
+ to register.
+ :param bool enable: If ``True``, immediately enable the
+ plugin after registration.
"""
- Instantiate a new plugin and store the given configuration.
+ register_plugin(plugin)
+ if enable:
+ self.enable(plugin.name)
+
+ def enable(self, name, config=None, enabled=None):
+ """Enable a plugin, including any dependencies.
- Arguments:
- xmpp -- The main SleekXMPP instance.
- config -- A dictionary of configuration values.
+ :param string name: The short name of the plugin.
+ :param dict config: Optional settings dictionary for
+ configuring plugin behaviour.
"""
+ top_level = False
+ if enabled is None:
+ enabled = set()
+
+ with self._plugin_lock:
+ if name not in self._enabled:
+ enabled.add(name)
+ self._enabled.add(name)
+ if not self.registered(name):
+ load_plugin(name)
+
+ plugin_class = PLUGIN_REGISTRY.get(name, None)
+ if not plugin_class:
+ raise PluginNotFound(name)
+
+ if config is None:
+ config = self.config.get(name, None)
+
+ plugin = plugin_class(self.xmpp, config)
+ self._plugins[name] = plugin
+ for dep in plugin.dependencies:
+ self.enable(dep, enabled=enabled)
+ plugin.plugin_init()
+ log.debug("Loaded Plugin: %s", plugin.description)
+
+ if top_level:
+ for name in enabled:
+ if hasattr(self.plugins[name], 'old_style'):
+ # Older style plugins require post_init()
+ # to run just before stream processing begins,
+ # so we don't call it here.
+ pass
+ self.plugins[name].post_init()
+
+ def enable_all(self, names=None, config=None):
+ """Enable all registered plugins.
+
+ :param list names: A list of plugin names to enable. If
+ none are provided, all registered plugins
+ will be enabled.
+ :param dict config: A dictionary mapping plugin names to
+ configuration dictionaries, as used by
+ :meth:`~PluginManager.enable`.
+ """
+ names = names if names else PLUGIN_REGISTRY.keys()
if config is None:
config = {}
- self.xep = None
- self.rfc = None
- self.description = 'Base Plugin'
- self.xmpp = xmpp
- self.config = config
- self.post_inited = False
- self.enable = config.get('enable', True)
- if self.enable:
- self.plugin_init()
+ for name in names:
+ self.enable(name, config.get(name, {}))
- def plugin_init(self):
+ def enabled(self, name):
+ """Check if a plugin has been enabled.
+
+ :param string name: The name of the plugin to check.
+ :return: boolean
+ """
+ return name in self._enabled
+
+ def registered(self, name):
+ """Check if a plugin has been registered.
+
+ :param string name: The name of the plugin to check.
+ :return: boolean
+ """
+ return name in PLUGIN_REGISTRY
+
+ def disable(self, name, _disabled=None):
+ """Disable a plugin, including any dependent upon it.
+
+ :param string name: The name of the plugin to disable.
+ :param set _disabled: Private set used to track the
+ disabled status of plugins during
+ the cascading process.
+ """
+ if _disabled is None:
+ _disabled = set()
+ with self._plugin_lock:
+ if name not in _disabled and name in self._enabled:
+ _disabled.add(name)
+ plugin = self._plugins.get(name, None)
+ if plugin is None:
+ raise PluginNotFound(name)
+ for dep in PLUGIN_DEPENDENTS[name]:
+ self.disable(dep, _disabled)
+ plugin.plugin_end()
+ if name in self._enabled:
+ self._enabled.remove(name)
+ del self._plugins[name]
+
+ def __keys__(self):
+ """Return the set of enabled plugins."""
+ return self._plugins.keys()
+
+ def __getitem__(self, name):
"""
- Initialize plugin state, such as registering any stream or
- event handlers, or new stanza types.
+ Allow plugins to be accessed through the manager as if
+ it were a dictionary.
"""
+ plugin = self._plugins.get(name, None)
+ if plugin is None:
+ raise PluginNotFound(name)
+ return plugin
+
+ def __iter__(self):
+ """Return an iterator over the set of enabled plugins."""
+ return self._plugins.__iter__()
+
+ def __len__(self):
+ """Return the number of enabled plugins."""
+ return len(self._plugins)
+
+
+class BasePlugin(object):
+
+ #: A short name for the plugin based on the implemented specification.
+ #: For example, a plugin for XEP-0030 would use `'xep_0030'`.
+ name = ''
+
+ #: A longer name for the plugin, describing its purpose. For example,
+ #: a plugin for XEP-0030 would use `'Service Discovery'` as its
+ #: description value.
+ description = ''
+
+ #: Some plugins may depend on others in order to function properly.
+ #: Any plugin names included in :attr:`~BasePlugin.dependencies` will
+ #: be initialized as needed if this plugin is enabled.
+ dependencies = set()
+
+ def __init__(self, xmpp, config=None):
+ self.xmpp = xmpp
+
+ #: A plugin's behaviour may be configurable, in which case those
+ #: configuration settings will be provided as a dictionary.
+ self.config = config if config is not None else {}
+
+ def plugin_init(self):
+ """Initialize plugin state, such as registering event handlers."""
+ pass
+
+ def plugin_end(self):
+ """Cleanup plugin state, and prepare for plugin removal."""
pass
def post_init(self):
+ """Initialize any cross-plugin state.
+
+ Only needed if the plugin has circular dependencies.
"""
- Perform any cross-plugin interactions, such as registering
- service discovery identities or items.
- """
- self.post_inited = True
+ pass
+
+
+base_plugin = BasePlugin
diff --git a/sleekxmpp/plugins/xep_0004/__init__.py b/sleekxmpp/plugins/xep_0004/__init__.py
index aad4e15f..2cd18ec8 100644
--- a/sleekxmpp/plugins/xep_0004/__init__.py
+++ b/sleekxmpp/plugins/xep_0004/__init__.py
@@ -6,6 +6,17 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0004.stanza import Form
from sleekxmpp.plugins.xep_0004.stanza import FormField, FieldOption
-from sleekxmpp.plugins.xep_0004.dataforms import xep_0004
+from sleekxmpp.plugins.xep_0004.dataforms import XEP_0004
+
+
+register_plugin(XEP_0004)
+
+
+# Retain some backwards compatibility
+xep_0004 = XEP_0004
+xep_0004.makeForm = xep_0004.make_form
+xep_0004.buildForm = xep_0004.build_form
diff --git a/sleekxmpp/plugins/xep_0004/dataforms.py b/sleekxmpp/plugins/xep_0004/dataforms.py
index 5414be5c..1097bd29 100644
--- a/sleekxmpp/plugins/xep_0004/dataforms.py
+++ b/sleekxmpp/plugins/xep_0004/dataforms.py
@@ -6,29 +6,27 @@
See the file LICENSE for copying permission.
"""
-import copy
-
-from sleekxmpp.thirdparty import OrderedDict
-
from sleekxmpp import Message
-from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0004 import stanza
from sleekxmpp.plugins.xep_0004.stanza import Form, FormField, FieldOption
-class xep_0004(base_plugin):
+class XEP_0004(BasePlugin):
+
"""
XEP-0004: Data Forms
"""
- def plugin_init(self):
- self.xep = '0004'
- self.description = 'Data Forms'
- self.stanza = stanza
+ name = 'xep_0004'
+ description = 'XEP-0004: Data Forms'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+ def plugin_init(self):
self.xmpp.registerHandler(
Callback('Data Form',
StanzaPath('message/form'),
@@ -38,6 +36,8 @@ class xep_0004(base_plugin):
register_stanza_plugin(Form, FormField, iterable=True)
register_stanza_plugin(Message, Form)
+ self.xmpp['xep_0030'].add_feature('jabber:x:data')
+
def make_form(self, ftype='form', title='', instructions=''):
f = Form()
f['type'] = ftype
@@ -45,16 +45,8 @@ class xep_0004(base_plugin):
f['instructions'] = instructions
return f
- def post_init(self):
- base_plugin.post_init(self)
- self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
-
def handle_form(self, message):
self.xmpp.event("message_xform", message)
def build_form(self, xml):
return Form(xml=xml)
-
-
-xep_0004.makeForm = xep_0004.make_form
-xep_0004.buildForm = xep_0004.build_form
diff --git a/sleekxmpp/plugins/xep_0004/stanza/field.py b/sleekxmpp/plugins/xep_0004/stanza/field.py
index 8131233c..1e175966 100644
--- a/sleekxmpp/plugins/xep_0004/stanza/field.py
+++ b/sleekxmpp/plugins/xep_0004/stanza/field.py
@@ -79,19 +79,21 @@ class FormField(ElementBase):
reqXML = self.xml.find('{%s}required' % self.namespace)
return reqXML is not None
- def get_value(self):
+ def get_value(self, convert=True):
valsXML = self.xml.findall('{%s}value' % self.namespace)
if len(valsXML) == 0:
return None
elif self._type == 'boolean':
- return valsXML[0].text in self.true_values
+ if convert:
+ return valsXML[0].text in self.true_values
+ return valsXML[0].text
elif self._type in self.multi_value_types or len(valsXML) > 1:
values = []
for valXML in valsXML:
if valXML.text is None:
valXML.text = ''
values.append(valXML.text)
- if self._type == 'text-multi':
+ if self._type == 'text-multi' and convert:
values = "\n".join(values)
return values
else:
@@ -136,6 +138,8 @@ class FormField(ElementBase):
valXML.text = '0'
self.xml.append(valXML)
elif self._type in self.multi_value_types or self._type in ('', None):
+ if isinstance(value, bool):
+ value = [value]
if not isinstance(value, list):
value = value.replace('\r', '')
value = value.split('\n')
diff --git a/sleekxmpp/plugins/xep_0009/__init__.py b/sleekxmpp/plugins/xep_0009/__init__.py
index 2cd14170..0ce3cf2c 100644
--- a/sleekxmpp/plugins/xep_0009/__init__.py
+++ b/sleekxmpp/plugins/xep_0009/__init__.py
@@ -6,6 +6,15 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0009 import stanza
-from sleekxmpp.plugins.xep_0009.rpc import xep_0009
+from sleekxmpp.plugins.xep_0009.rpc import XEP_0009
from sleekxmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse
+
+
+register_plugin(XEP_0009)
+
+
+# Retain some backwards compatibility
+xep_0009 = XEP_0009
diff --git a/sleekxmpp/plugins/xep_0009/binding.py b/sleekxmpp/plugins/xep_0009/binding.py
index b4395707..a55993ad 100644
--- a/sleekxmpp/plugins/xep_0009/binding.py
+++ b/sleekxmpp/plugins/xep_0009/binding.py
@@ -6,10 +6,14 @@
See the file LICENSE for copying permission.
"""
-from xml.etree import cElementTree as ET
+from sleekxmpp.xmlstream import ET
import base64
import logging
import time
+import sys
+
+if sys.version_info > (3, 0):
+ unicode = str
log = logging.getLogger(__name__)
@@ -54,7 +58,7 @@ def _py2xml(*args):
boolean = ET.Element("{%s}boolean" % _namespace)
boolean.text = str(int(x))
val.append(boolean)
- elif type(x) is str:
+ elif type(x) in (str, unicode):
string = ET.Element("{%s}string" % _namespace)
string.text = x
val.append(string)
@@ -152,7 +156,7 @@ class rpctime(object):
def __init__(self,data=None):
#assume string data is in iso format YYYYMMDDTHH:MM:SS
- if type(data) is str:
+ if type(data) in (str, unicode):
self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
elif type(data) is time.struct_time:
self.timestamp = data
diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py
index 4f749f30..4e1c538b 100644
--- a/sleekxmpp/plugins/xep_0009/rpc.py
+++ b/sleekxmpp/plugins/xep_0009/rpc.py
@@ -6,28 +6,28 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.plugins import base
-from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
-from sleekxmpp.stanza.iq import Iq
-from sleekxmpp.xmlstream.handler.callback import Callback
-from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
-from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
-from xml.etree import cElementTree as ET
import logging
+from sleekxmpp import Iq
+from sleekxmpp.xmlstream import ET, register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0009 import stanza
+from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
log = logging.getLogger(__name__)
+class XEP_0009(BasePlugin):
-class xep_0009(base.base_plugin):
+ name = 'xep_0009'
+ description = 'XEP-0009: Jabber-RPC'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
def plugin_init(self):
- self.xep = '0009'
- self.description = 'Jabber-RPC'
- #self.stanza = sleekxmpp.plugins.xep_0009.stanza
-
register_stanza_plugin(Iq, RPCQuery)
register_stanza_plugin(RPCQuery, MethodCall)
register_stanza_plugin(RPCQuery, MethodResponse)
@@ -51,10 +51,8 @@ class xep_0009(base.base_plugin):
self.xmpp.add_event_handler('error', self._handle_error)
#self.activeCalls = []
- def post_init(self):
- base.base_plugin.post_init(self)
- self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
- self.xmpp.plugin['xep_0030'].add_identity('automation','rpc')
+ self.xmpp['xep_0030'].add_feature('jabber:iq:rpc')
+ self.xmpp['xep_0030'].add_identity('automation','rpc')
def make_iq_method_call(self, pto, pmethod, params):
iq = self.xmpp.makeIqSet()
@@ -218,4 +216,3 @@ class xep_0009(base.base_plugin):
def _extract_method(self, stanza):
xml = ET.fromstring("%s" % stanza)
return xml.find("./methodCall/methodName").text
-
diff --git a/sleekxmpp/plugins/xep_0012.py b/sleekxmpp/plugins/xep_0012.py
index c5532bd4..01fb60a8 100644
--- a/sleekxmpp/plugins/xep_0012.py
+++ b/sleekxmpp/plugins/xep_0012.py
@@ -9,11 +9,11 @@
from datetime import datetime
import logging
-from . import base
-from .. stanza.iq import Iq
-from .. xmlstream.handler.callback import Callback
-from .. xmlstream.matcher.xpath import MatchXPath
-from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin, register_plugin
+from sleekxmpp import Iq
+from sleekxmpp.xmlstream.handler.callback import Callback
+from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
+from sleekxmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin
log = logging.getLogger(__name__)
@@ -40,12 +40,18 @@ class LastActivity(ElementBase):
def del_status(self):
self.xml.text = ''
-class xep_0012(base.base_plugin):
+
+class XEP_0012(BasePlugin):
+
"""
XEP-0012 Last Activity
"""
+
+ name = 'xep_0012'
+ description = 'XEP-0012: Last Activity'
+ dependencies = set(['xep_0030'])
+
def plugin_init(self):
- self.description = "Last Activity"
self.xep = "0012"
self.xmpp.registerHandler(
@@ -57,9 +63,6 @@ class xep_0012(base.base_plugin):
self.xmpp.add_event_handler('last_activity_request', self.handle_last_activity)
-
- def post_init(self):
- base.base_plugin.post_init(self)
if self.xmpp.is_component:
# We are a component, so we track the uptime
self.xmpp.add_event_handler("session_start", self._reset_uptime)
@@ -113,3 +116,7 @@ class xep_0012(base.base_plugin):
id = iq.get('id')
result = iq.send()
return result['last_activity']['seconds']
+
+
+xep_0012 = XEP_0012
+register_plugin(XEP_0012)
diff --git a/sleekxmpp/plugins/xep_0030/__init__.py b/sleekxmpp/plugins/xep_0030/__init__.py
index 2e183852..0d1de65b 100644
--- a/sleekxmpp/plugins/xep_0030/__init__.py
+++ b/sleekxmpp/plugins/xep_0030/__init__.py
@@ -6,7 +6,18 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0030 import stanza
from sleekxmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems
from sleekxmpp.plugins.xep_0030.static import StaticDisco
-from sleekxmpp.plugins.xep_0030.disco import xep_0030
+from sleekxmpp.plugins.xep_0030.disco import XEP_0030
+
+
+register_plugin(XEP_0030)
+
+# Retain some backwards compatibility
+xep_0030 = XEP_0030
+XEP_0030.getInfo = XEP_0030.get_info
+XEP_0030.getItems = XEP_0030.get_items
+XEP_0030.make_static = XEP_0030.restore_defaults
diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py
index 53086d4e..a5e8fd1c 100644
--- a/sleekxmpp/plugins/xep_0030/disco.py
+++ b/sleekxmpp/plugins/xep_0030/disco.py
@@ -8,20 +8,19 @@
import logging
-import sleekxmpp
from sleekxmpp import Iq
-from sleekxmpp.exceptions import XMPPError
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
-from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco
+from sleekxmpp.xmlstream import register_stanza_plugin, JID
+from sleekxmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems
+from sleekxmpp.plugins.xep_0030 import StaticDisco
log = logging.getLogger(__name__)
-class xep_0030(base_plugin):
+class XEP_0030(BasePlugin):
"""
XEP-0030: Service Discovery
@@ -85,14 +84,15 @@ class xep_0030(base_plugin):
add_item --
"""
+ name = 'xep_0030'
+ description = 'XEP-0030: Service Discovery'
+ dependencies = set()
+ stanza = stanza
+
def plugin_init(self):
"""
Start the XEP-0030 plugin.
"""
- self.xep = '0030'
- self.description = 'Service Discovery'
- self.stanza = sleekxmpp.plugins.xep_0030.stanza
-
self.xmpp.register_handler(
Callback('Disco Info',
StanzaPath('iq/disco_info'),
@@ -106,25 +106,23 @@ class xep_0030(base_plugin):
register_stanza_plugin(Iq, DiscoInfo)
register_stanza_plugin(Iq, DiscoItems)
- self.static = StaticDisco(self.xmpp)
+ self.static = StaticDisco(self.xmpp, self)
+
+ self.use_cache = self.config.get('use_cache', True)
+ self.wrap_results = self.config.get('wrap_results', False)
+
+ self._disco_ops = [
+ 'get_info', 'set_info', 'set_identities', 'set_features',
+ 'get_items', 'set_items', 'del_items', 'add_identity',
+ 'del_identity', 'add_feature', 'del_feature', 'add_item',
+ 'del_item', 'del_identities', 'del_features', 'cache_info',
+ 'get_cached_info', 'supports', 'has_identity']
- self._disco_ops = ['get_info', 'set_identities', 'set_features',
- 'get_items', 'set_items', 'del_items',
- 'add_identity', 'del_identity', 'add_feature',
- 'del_feature', 'add_item', 'del_item',
- 'del_identities', 'del_features']
self.default_handlers = {}
self._handlers = {}
for op in self._disco_ops:
self._add_disco_op(op, getattr(self.static, op))
- def post_init(self):
- """Handle cross-plugin dependencies."""
- base_plugin.post_init(self)
- if 'xep_0059' in self.xmpp.plugin:
- register_stanza_plugin(DiscoItems,
- self.xmpp['xep_0059'].stanza.Set)
-
def _add_disco_op(self, op, default_handler):
self.default_handlers[op] = default_handler
self._handlers[op] = {'global': default_handler,
@@ -237,7 +235,78 @@ class xep_0030(base_plugin):
self.del_node_handler(op, jid, node)
self.set_node_handler(op, jid, node, self.default_handlers[op])
- def get_info(self, jid=None, node=None, local=False, **kwargs):
+ def supports(self, jid=None, node=None, feature=None, local=False,
+ cached=True, ifrom=None):
+ """
+ Check if a JID supports a given feature.
+
+ Return values:
+ True -- The feature is supported
+ False -- The feature is not listed as supported
+ None -- Nothing could be found due to a timeout
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ feature -- The name of the feature to check.
+ 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.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ ifrom -- Specifiy the sender's JID.
+ """
+ data = {'feature': feature,
+ 'local': local,
+ 'cached': cached}
+ return self._run_node_handler('supports', jid, node, ifrom, data)
+
+ def has_identity(self, jid=None, node=None, category=None, itype=None,
+ lang=None, local=False, cached=True, ifrom=None):
+ """
+ Check if a JID provides a given identity.
+
+ Return values:
+ True -- The identity is provided
+ False -- The identity is not listed
+ None -- Nothing could be found due to a timeout
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ category -- The category of the identity to check.
+ itype -- The type of the identity to check.
+ lang -- The language of the identity to check.
+ 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.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ ifrom -- Specifiy the sender's JID.
+ """
+ data = {'category': category,
+ 'itype': itype,
+ 'lang': lang,
+ 'local': local,
+ 'cached': cached}
+ return self._run_node_handler('has_identity', jid, node, ifrom, data)
+
+ def get_info(self, jid=None, node=None, local=False,
+ cached=None, **kwargs):
"""
Retrieve the disco#info results from a given JID/node combination.
@@ -257,6 +326,13 @@ class xep_0030(base_plugin):
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
ifrom -- 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
@@ -266,11 +342,31 @@ class xep_0030(base_plugin):
received instead of blocking and waiting for
the reply.
"""
- if local or jid is None:
+ if jid is not None and not isinstance(jid, JID):
+ jid = JID(jid)
+ if self.xmpp.is_component:
+ if jid.domain == self.xmpp.boundjid.domain:
+ local = True
+ else:
+ if str(jid) == str(self.xmpp.boundjid):
+ local = True
+ jid = jid.full
+
+ if local or jid in (None, ''):
log.debug("Looking up local disco#info data " + \
"for %s, node %s.", jid, node)
- info = self._run_node_handler('get_info', jid, node, kwargs)
- return self._fix_default_info(info)
+ info = self._run_node_handler('get_info',
+ jid, node, kwargs.get('ifrom', None), kwargs)
+ info = self._fix_default_info(info)
+ return self._wrap(kwargs.get('ifrom', None), jid, info)
+
+ if cached:
+ log.debug("Looking up cached disco#info data " + \
+ "for %s, node %s.", jid, node)
+ info = self._run_node_handler('get_cached_info',
+ jid, node, kwargs.get('ifrom', None), kwargs)
+ if info is not None:
+ return self._wrap(kwargs.get('ifrom', None), jid, info)
iq = self.xmpp.Iq()
# Check dfrom parameter for backwards compatibility
@@ -282,6 +378,15 @@ class xep_0030(base_plugin):
block=kwargs.get('block', True),
callback=kwargs.get('callback', None))
+ def set_info(self, jid=None, node=None, info=None):
+ """
+ Set the disco#info data for a JID/node based on an existing
+ disco#info stanza.
+ """
+ if isinstance(info, Iq):
+ info = info['disco_info']
+ self._run_node_handler('set_info', jid, node, None, info)
+
def get_items(self, jid=None, node=None, local=False, **kwargs):
"""
Retrieve the disco#items results from a given JID/node combination.
@@ -314,7 +419,9 @@ class xep_0030(base_plugin):
Otherwise the parameter is ignored.
"""
if local or jid is None:
- return self._run_node_handler('get_items', jid, node, kwargs)
+ items = self._run_node_handler('get_items',
+ jid, node, kwargs.get('ifrom', None), kwargs)
+ return self._wrap(kwargs.get('ifrom', None), jid, items)
iq = self.xmpp.Iq()
# Check dfrom parameter for backwards compatibility
@@ -341,7 +448,7 @@ class xep_0030(base_plugin):
node -- Optional node to modify.
items -- A series of items in tuple format.
"""
- self._run_node_handler('set_items', jid, node, kwargs)
+ self._run_node_handler('set_items', jid, node, None, kwargs)
def del_items(self, jid=None, node=None, **kwargs):
"""
@@ -351,7 +458,7 @@ class xep_0030(base_plugin):
jid -- The JID to modify.
node -- Optional node to modify.
"""
- self._run_node_handler('del_items', jid, node, kwargs)
+ self._run_node_handler('del_items', jid, node, None, kwargs)
def add_item(self, jid='', name='', node=None, subnode='', ijid=None):
"""
@@ -372,7 +479,7 @@ class xep_0030(base_plugin):
kwargs = {'ijid': jid,
'name': name,
'inode': subnode}
- self._run_node_handler('add_item', ijid, node, kwargs)
+ self._run_node_handler('add_item', ijid, node, None, kwargs)
def del_item(self, jid=None, node=None, **kwargs):
"""
@@ -384,7 +491,7 @@ class xep_0030(base_plugin):
ijid -- The item's JID.
inode -- The item's node.
"""
- self._run_node_handler('del_item', jid, node, kwargs)
+ self._run_node_handler('del_item', jid, node, None, kwargs)
def add_identity(self, category='', itype='', name='',
node=None, jid=None, lang=None):
@@ -411,7 +518,7 @@ class xep_0030(base_plugin):
'itype': itype,
'name': name,
'lang': lang}
- self._run_node_handler('add_identity', jid, node, kwargs)
+ self._run_node_handler('add_identity', jid, node, None, kwargs)
def add_feature(self, feature, node=None, jid=None):
"""
@@ -423,7 +530,7 @@ class xep_0030(base_plugin):
jid -- The JID to modify.
"""
kwargs = {'feature': feature}
- self._run_node_handler('add_feature', jid, node, kwargs)
+ self._run_node_handler('add_feature', jid, node, None, kwargs)
def del_identity(self, jid=None, node=None, **kwargs):
"""
@@ -437,7 +544,7 @@ class xep_0030(base_plugin):
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)
+ self._run_node_handler('del_identity', jid, node, None, kwargs)
def del_feature(self, jid=None, node=None, **kwargs):
"""
@@ -448,7 +555,7 @@ class xep_0030(base_plugin):
node -- The node to modify.
feature -- The feature's namespace.
"""
- self._run_node_handler('del_feature', jid, node, kwargs)
+ self._run_node_handler('del_feature', jid, node, None, kwargs)
def set_identities(self, jid=None, node=None, **kwargs):
"""
@@ -463,7 +570,7 @@ class xep_0030(base_plugin):
identities -- A set of identities in tuple form.
lang -- Optional, xml:lang value.
"""
- self._run_node_handler('set_identities', jid, node, kwargs)
+ self._run_node_handler('set_identities', jid, node, None, kwargs)
def del_identities(self, jid=None, node=None, **kwargs):
"""
@@ -478,7 +585,7 @@ class xep_0030(base_plugin):
lang -- Optional. If given, only remove identities
using this xml:lang value.
"""
- self._run_node_handler('del_identities', jid, node, kwargs)
+ self._run_node_handler('del_identities', jid, node, None, kwargs)
def set_features(self, jid=None, node=None, **kwargs):
"""
@@ -490,7 +597,7 @@ class xep_0030(base_plugin):
node -- The node to modify.
features -- The new set of supported features.
"""
- self._run_node_handler('set_features', jid, node, kwargs)
+ self._run_node_handler('set_features', jid, node, None, kwargs)
def del_features(self, jid=None, node=None, **kwargs):
"""
@@ -500,9 +607,9 @@ class xep_0030(base_plugin):
jid -- The JID to modify.
node -- The node to modify.
"""
- self._run_node_handler('del_features', jid, node, kwargs)
+ self._run_node_handler('del_features', jid, node, None, kwargs)
- def _run_node_handler(self, htype, jid, node, data={}):
+ def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}):
"""
Execute the most specific node handler for the given
JID/node combination.
@@ -513,7 +620,10 @@ class xep_0030(base_plugin):
node -- The node requested.
data -- Optional, custom data to pass to the handler.
"""
- if jid is None:
+ if isinstance(jid, JID):
+ jid = jid.full
+
+ if jid in (None, ''):
if self.xmpp.is_component:
jid = self.xmpp.boundjid.full
else:
@@ -521,14 +631,28 @@ class xep_0030(base_plugin):
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)
- else:
- return None
+ try:
+ args = (jid, node, ifrom, data)
+ if self._handlers[htype]['node'].get((jid, node), False):
+ return self._handlers[htype]['node'][(jid, node)](*args)
+ elif self._handlers[htype]['jid'].get(jid, False):
+ return self._handlers[htype]['jid'][jid](*args)
+ elif self._handlers[htype]['global']:
+ return self._handlers[htype]['global'](*args)
+ else:
+ return None
+ except TypeError:
+ # To preserve backward compatibility, drop the ifrom parameter
+ # for existing handlers that don't understand it.
+ args = (jid, node, data)
+ if self._handlers[htype]['node'].get((jid, node), False):
+ return self._handlers[htype]['node'][(jid, node)](*args)
+ elif self._handlers[htype]['jid'].get(jid, False):
+ return self._handlers[htype]['jid'][jid](*args)
+ elif self._handlers[htype]['global']:
+ return self._handlers[htype]['global'](*args)
+ else:
+ return None
def _handle_disco_info(self, iq):
"""
@@ -550,6 +674,7 @@ class xep_0030(base_plugin):
info = self._run_node_handler('get_info',
jid,
iq['disco_info']['node'],
+ iq['from'],
iq)
if isinstance(info, Iq):
info.send()
@@ -560,8 +685,20 @@ class xep_0030(base_plugin):
iq.set_payload(info.xml)
iq.send()
elif iq['type'] == 'result':
- log.debug("Received disco info result from" + \
- "%s to %s.", iq['from'], iq['to'])
+ log.debug("Received disco info result from " + \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.use_cache:
+ log.debug("Caching disco info result from " \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.xmpp.is_component:
+ ito = iq['to'].full
+ else:
+ ito = None
+ self._run_node_handler('cache_info',
+ iq['from'].full,
+ iq['disco_info']['node'],
+ ito,
+ iq)
self.xmpp.event('disco_info', iq)
def _handle_disco_items(self, iq):
@@ -583,6 +720,7 @@ class xep_0030(base_plugin):
items = self._run_node_handler('get_items',
jid,
iq['disco_items']['node'],
+ iq['from'].full,
iq)
if isinstance(items, Iq):
items.send()
@@ -592,7 +730,7 @@ class xep_0030(base_plugin):
iq.set_payload(items.xml)
iq.send()
elif iq['type'] == 'result':
- log.debug("Received disco items result from" + \
+ log.debug("Received disco items result from " + \
"%s to %s.", iq['from'], iq['to'])
self.xmpp.event('disco_items', iq)
@@ -607,24 +745,43 @@ class xep_0030(base_plugin):
Arguments:
info -- The disco#info quest (not the full Iq stanza) to modify.
"""
+ result = info
+ if isinstance(info, Iq):
+ info = info['disco_info']
if not info['node']:
if not info['identities']:
if self.xmpp.is_component:
- log.debug("No identity found for this entity." + \
+ log.debug("No identity found for this entity. " + \
"Using default component identity.")
info.add_identity('component', 'generic')
else:
- log.debug("No identity found for this entity." + \
+ log.debug("No identity found for this entity. " + \
"Using default client identity.")
info.add_identity('client', 'bot')
if not info['features']:
- log.debug("No features found for this entity." + \
+ log.debug("No features found for this entity. " + \
"Using default disco#info feature.")
info.add_feature(info.namespace)
- return info
+ return result
+ def _wrap(self, ito, ifrom, payload, force=False):
+ """
+ Ensure that results are wrapped in an Iq stanza
+ if self.wrap_results has been set to True.
-# Retain some backwards compatibility
-xep_0030.getInfo = xep_0030.get_info
-xep_0030.getItems = xep_0030.get_items
-xep_0030.make_static = xep_0030.restore_defaults
+ Arguments:
+ ito -- The JID to use as the 'to' value
+ ifrom -- The JID to use as the 'from' value
+ payload -- The disco data to wrap
+ force -- Force wrapping, regardless of self.wrap_results
+ """
+ if (force or self.wrap_results) and not isinstance(payload, Iq):
+ iq = self.xmpp.Iq()
+ # Since we're simulating a result, we have to treat
+ # the 'from' and 'to' values opposite the normal way.
+ iq['to'] = self.xmpp.boundjid if ito is None else ito
+ iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom
+ iq['type'] = 'result'
+ iq.append(payload)
+ return iq
+ return payload
diff --git a/sleekxmpp/plugins/xep_0030/stanza/info.py b/sleekxmpp/plugins/xep_0030/stanza/info.py
index 6764acbb..25d1d07f 100644
--- a/sleekxmpp/plugins/xep_0030/stanza/info.py
+++ b/sleekxmpp/plugins/xep_0030/stanza/info.py
@@ -146,7 +146,7 @@ class DiscoInfo(ElementBase):
return True
return False
- def get_identities(self, lang=None):
+ def get_identities(self, lang=None, dedupe=True):
"""
Return a set of all identities in tuple form as so:
(category, type, lang, name)
@@ -155,17 +155,25 @@ class DiscoInfo(ElementBase):
that language.
Arguments:
- lang -- Optional, standard xml:lang value.
+ lang -- Optional, standard xml:lang value.
+ dedupe -- If True, de-duplicate identities, otherwise
+ return a list of all identities.
"""
- identities = set()
+ if dedupe:
+ identities = set()
+ else:
+ identities = []
for id_xml in self.findall('{%s}identity' % self.namespace):
xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None)
if lang is None or xml_lang == lang:
- identities.add((
- id_xml.attrib['category'],
- id_xml.attrib['type'],
- id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
- id_xml.attrib.get('name', None)))
+ id = (id_xml.attrib['category'],
+ id_xml.attrib['type'],
+ id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
+ id_xml.attrib.get('name', None))
+ if dedupe:
+ identities.add(id)
+ else:
+ identities.append(id)
return identities
def set_identities(self, identities, lang=None):
@@ -237,11 +245,17 @@ class DiscoInfo(ElementBase):
return True
return False
- def get_features(self):
+ def get_features(self, dedupe=True):
"""Return the set of all supported features."""
- features = set()
+ if dedupe:
+ features = set()
+ else:
+ features = []
for feature_xml in self.findall('{%s}feature' % self.namespace):
- features.add(feature_xml.attrib['var'])
+ if dedupe:
+ features.add(feature_xml.attrib['var'])
+ else:
+ features.append(feature_xml.attrib['var'])
return features
def set_features(self, features):
diff --git a/sleekxmpp/plugins/xep_0030/stanza/items.py b/sleekxmpp/plugins/xep_0030/stanza/items.py
index a1fb819c..512f2336 100644
--- a/sleekxmpp/plugins/xep_0030/stanza/items.py
+++ b/sleekxmpp/plugins/xep_0030/stanza/items.py
@@ -6,7 +6,7 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.xmlstream import ElementBase, ET
+from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
class DiscoItems(ElementBase):
@@ -78,13 +78,11 @@ class DiscoItems(ElementBase):
"""
if (jid, node) not in self._items:
self._items.add((jid, node))
- item_xml = ET.Element('{%s}item' % self.namespace)
- item_xml.attrib['jid'] = jid
- if name:
- item_xml.attrib['name'] = name
- if node:
- item_xml.attrib['node'] = node
- self.xml.append(item_xml)
+ item = DiscoItem(parent=self)
+ item['jid'] = jid
+ item['node'] = node
+ item['name'] = name
+ self.iterables.append(item)
return True
return False
@@ -108,11 +106,9 @@ class DiscoItems(ElementBase):
def get_items(self):
"""Return all items."""
items = set()
- for item_xml in self.findall('{%s}item' % self.namespace):
- item = (item_xml.attrib['jid'],
- item_xml.attrib.get('node'),
- item_xml.attrib.get('name'))
- items.add(item)
+ for item in self['substanzas']:
+ if isinstance(item, DiscoItem):
+ items.add((item['jid'], item['node'], item['name']))
return items
def set_items(self, items):
@@ -132,5 +128,24 @@ class DiscoItems(ElementBase):
def del_items(self):
"""Remove all items."""
self._items = set()
- for item_xml in self.findall('{%s}item' % self.namespace):
- self.xml.remove(item_xml)
+ for item in self['substanzas']:
+ if isinstance(item, DiscoItem):
+ self.xml.remove(item.xml)
+
+
+class DiscoItem(ElementBase):
+ name = 'item'
+ namespace = 'http://jabber.org/protocol/disco#items'
+ plugin_attrib = name
+ interfaces = set(('jid', 'node', 'name'))
+
+ def get_node(self):
+ """Return the item's node name or ``None``."""
+ return self._get_attr('node', None)
+
+ def get_name(self):
+ """Return the item's human readable name, or ``None``."""
+ return self._get_attr('name', None)
+
+
+register_stanza_plugin(DiscoItems, DiscoItem, iterable=True)
diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py
index 7e7f0353..7306461a 100644
--- a/sleekxmpp/plugins/xep_0030/static.py
+++ b/sleekxmpp/plugins/xep_0030/static.py
@@ -7,14 +7,11 @@
"""
import logging
+import threading
-import sleekxmpp
from sleekxmpp import Iq
-from sleekxmpp.exceptions import XMPPError
-from sleekxmpp.plugins.base import base_plugin
-from sleekxmpp.xmlstream.handler import Callback
-from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout
+from sleekxmpp.xmlstream import JID
from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems
@@ -38,7 +35,7 @@ class StaticDisco(object):
xmpp -- The main SleekXMPP object.
"""
- def __init__(self, xmpp):
+ def __init__(self, xmpp, disco):
"""
Create a static disco interface. Sets of disco#info and
disco#items are maintained for every given JID and node
@@ -50,8 +47,10 @@ class StaticDisco(object):
"""
self.nodes = {}
self.xmpp = xmpp
+ self.disco = disco
+ self.lock = threading.RLock()
- def add_node(self, jid=None, node=None):
+ def add_node(self, jid=None, node=None, ifrom=None):
"""
Create a new set of stanzas for the provided
JID and node combination.
@@ -60,83 +59,218 @@ class StaticDisco(object):
jid -- The JID that will own the new stanzas.
node -- The node that will own the new stanzas.
"""
- if jid is None:
- jid = self.xmpp.boundjid.full
- if node is None:
- node = ''
- if (jid, node) not in self.nodes:
- self.nodes[(jid, node)] = {'info': DiscoInfo(),
- 'items': DiscoItems()}
- self.nodes[(jid, node)]['info']['node'] = node
- self.nodes[(jid, node)]['items']['node'] = node
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(),
+ 'items': DiscoItems()}
+ self.nodes[(jid, node, ifrom)]['info']['node'] = node
+ self.nodes[(jid, node, ifrom)]['items']['node'] = node
+
+ def get_node(self, jid=None, node=None, ifrom=None):
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ self.add_node(jid, node, ifrom)
+ return self.nodes[(jid, node, ifrom)]
+
+ def node_exists(self, jid=None, node=None, ifrom=None):
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ return False
+ return True
# =================================================================
# Node Handlers
#
- # Each handler accepts three arguments: jid, node, and data.
- # The jid and node parameters together determine the set of
- # info and items stanzas that will be retrieved or added.
- # The data parameter is a dictionary with additional paramters
- # that will be passed to other calls.
+ # Each handler accepts four arguments: jid, node, ifrom, and data.
+ # The jid and node parameters together determine the set of info
+ # and items stanzas that will be retrieved or added. Additionally,
+ # the ifrom value allows for cached results when results vary based
+ # on the requester's JID. The data parameter is a dictionary with
+ # additional parameters that will be passed to other calls.
+ #
+ # This implementation does not allow different responses based on
+ # the requester's JID, except for cached results. To do that,
+ # register a custom node handler.
- def get_info(self, jid, node, data):
+ def supports(self, jid, node, ifrom, data):
+ """
+ Check if a JID supports a given feature.
+
+ The data parameter may provide:
+ feature -- The feature to check for support.
+ 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.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ feature = data.get('feature', None)
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ if not feature:
+ return False
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ features = info['disco_info']['features']
+ return feature in features
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def has_identity(self, jid, node, ifrom, data):
+ """
+ Check if a JID has a given identity.
+
+ The data parameter may provide:
+ category -- The category of the identity to check.
+ itype -- The type of the identity to check.
+ lang -- The language of the identity to check.
+ 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.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ identity = (data.get('category', None),
+ data.get('itype', None),
+ data.get('lang', None))
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ if node in (None, ''):
+ info = self.caps.get_caps(jid)
+ if info and identity in info['identities']:
+ return True
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ trunc = lambda i: (i[0], i[1], i[2])
+ return identity in map(trunc, info['disco_info']['identities'])
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def get_info(self, jid, node, ifrom, data):
"""
Return the stored info data for the requested JID/node combination.
The data parameter is not used.
"""
- if (jid, node) not in self.nodes:
- if not node:
- return DiscoInfo()
+ with self.lock:
+ if not self.node_exists(jid, node):
+ if not node:
+ return DiscoInfo()
+ else:
+ raise XMPPError(condition='item-not-found')
else:
- raise XMPPError(condition='item-not-found')
- else:
- return self.nodes[(jid, node)]['info']
+ return self.get_node(jid, node)['info']
- def del_info(self, jid, node, data):
+ def set_info(self, jid, node, ifrom, data):
+ """
+ Set the entire info stanza for a JID/node at once.
+
+ The data parameter is a disco#info substanza.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'] = data
+
+ def del_info(self, jid, node, ifrom, data):
"""
Reset the info stanza for a given JID/node combination.
The data parameter is not used.
"""
- if (jid, node) in self.nodes:
- self.nodes[(jid, node)]['info'] = DiscoInfo()
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'] = DiscoInfo()
- def get_items(self, jid, node, data):
+ def get_items(self, jid, node, ifrom, data):
"""
Return the stored items data for the requested JID/node combination.
The data parameter is not used.
"""
- if (jid, node) not in self.nodes:
- if not node:
- return DiscoInfo()
+ with self.lock:
+ if not self.node_exists(jid, node):
+ if not node:
+ return DiscoInfo()
+ else:
+ raise XMPPError(condition='item-not-found')
else:
- raise XMPPError(condition='item-not-found')
- else:
- return self.nodes[(jid, node)]['items']
+ return self.get_node(jid, node)['items']
- def set_items(self, jid, node, data):
+ def set_items(self, jid, node, ifrom, data):
"""
Replace the stored items data for a JID/node combination.
- The data parameter may provided:
+ The data parameter may provide:
items -- A set of items in tuple format.
"""
- items = data.get('items', set())
- self.add_node(jid, node)
- self.nodes[(jid, node)]['items']['items'] = items
+ with self.lock:
+ items = data.get('items', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['items']['items'] = items
- def del_items(self, jid, node, data):
+ def del_items(self, jid, node, ifrom, data):
"""
Reset the items stanza for a given JID/node combination.
The data parameter is not used.
"""
- if (jid, node) in self.nodes:
- self.nodes[(jid, node)]['items'] = DiscoItems()
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['items'] = DiscoItems()
- def add_identity(self, jid, node, data):
+ def add_identity(self, jid, node, ifrom, data):
"""
Add a new identity to te JID/node combination.
@@ -146,14 +280,15 @@ class StaticDisco(object):
name -- Optional human readable name for this identity.
lang -- Optional standard xml:lang value.
"""
- self.add_node(jid, node)
- self.nodes[(jid, node)]['info'].add_identity(
- data.get('category', ''),
- data.get('itype', ''),
- data.get('name', None),
- data.get('lang', None))
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'].add_identity(
+ data.get('category', ''),
+ data.get('itype', ''),
+ data.get('name', None),
+ data.get('lang', None))
- def set_identities(self, jid, node, data):
+ def set_identities(self, jid, node, ifrom, data):
"""
Add or replace all identities for a JID/node combination.
@@ -161,11 +296,12 @@ class StaticDisco(object):
identities -- A list of identities in tuple form:
(category, type, name, lang)
"""
- identities = data.get('identities', set())
- self.add_node(jid, node)
- self.nodes[(jid, node)]['info']['identities'] = identities
+ with self.lock:
+ identities = data.get('identities', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info']['identities'] = identities
- def del_identity(self, jid, node, data):
+ def del_identity(self, jid, node, ifrom, data):
"""
Remove an identity from a JID/node combination.
@@ -175,67 +311,72 @@ class StaticDisco(object):
name -- Optional human readable name for this identity.
lang -- Optional, standard xml:lang value.
"""
- if (jid, node) not in self.nodes:
- return
- self.nodes[(jid, node)]['info'].del_identity(
- data.get('category', ''),
- data.get('itype', ''),
- data.get('name', None),
- data.get('lang', None))
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'].del_identity(
+ data.get('category', ''),
+ data.get('itype', ''),
+ data.get('name', None),
+ data.get('lang', None))
- def del_identities(self, jid, node, data):
+ def del_identities(self, jid, node, ifrom, data):
"""
Remove all identities from a JID/node combination.
The data parameter is not used.
"""
- if (jid, node) not in self.nodes:
- return
- del self.nodes[(jid, node)]['info']['identities']
+ with self.lock:
+ if self.node_exists(jid, node):
+ del self.get_node(jid, node)['info']['identities']
- def add_feature(self, jid, node, data):
+ def add_feature(self, jid, node, ifrom, data):
"""
Add a feature to a JID/node combination.
The data parameter should include:
feature -- The namespace of the supported feature.
"""
- self.add_node(jid, node)
- self.nodes[(jid, node)]['info'].add_feature(data.get('feature', ''))
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'].add_feature(
+ data.get('feature', ''))
- def set_features(self, jid, node, data):
+ def set_features(self, jid, node, ifrom, data):
"""
Add or replace all features for a JID/node combination.
The data parameter should include:
features -- The new set of supported features.
"""
- features = data.get('features', set())
- self.add_node(jid, node)
- self.nodes[(jid, node)]['info']['features'] = features
+ with self.lock:
+ features = data.get('features', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info']['features'] = features
- def del_feature(self, jid, node, data):
+ def del_feature(self, jid, node, ifrom, data):
"""
Remove a feature from a JID/node combination.
The data parameter should include:
feature -- The namespace of the removed feature.
"""
- if (jid, node) not in self.nodes:
- return
- self.nodes[(jid, node)]['info'].del_feature(data.get('feature', ''))
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'].del_feature(
+ data.get('feature', ''))
- def del_features(self, jid, node, data):
+ def del_features(self, jid, node, ifrom, data):
"""
Remove all features from a JID/node combination.
The data parameter is not used.
"""
- if (jid, node) not in self.nodes:
- return
- del self.nodes[(jid, node)]['info']['features']
+ with self.lock:
+ if not self.node_exists(jid, node):
+ return
+ del self.get_node(jid, node)['info']['features']
- def add_item(self, jid, node, data):
+ def add_item(self, jid, node, ifrom, data):
"""
Add an item to a JID/node combination.
@@ -245,13 +386,14 @@ class StaticDisco(object):
non-addressable items.
name -- Optional human readable name for the item.
"""
- self.add_node(jid, node)
- self.nodes[(jid, node)]['items'].add_item(
- data.get('ijid', ''),
- node=data.get('inode', ''),
- name=data.get('name', ''))
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['items'].add_item(
+ data.get('ijid', ''),
+ node=data.get('inode', ''),
+ name=data.get('name', ''))
- def del_item(self, jid, node, data):
+ def del_item(self, jid, node, ifrom, data):
"""
Remove an item from a JID/node combination.
@@ -259,7 +401,38 @@ class StaticDisco(object):
ijid -- JID of the item to remove.
inode -- Optional extra identifying information.
"""
- if (jid, node) in self.nodes:
- self.nodes[(jid, node)]['items'].del_item(
- data.get('ijid', ''),
- node=data.get('inode', None))
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['items'].del_item(
+ data.get('ijid', ''),
+ node=data.get('inode', None))
+
+ def cache_info(self, jid, node, ifrom, data):
+ """
+ Cache disco information for an external JID.
+
+ The data parameter is the Iq result stanza
+ containing the disco info to cache, or
+ the disco#info substanza itself.
+ """
+ with self.lock:
+ if isinstance(data, Iq):
+ data = data['disco_info']
+
+ self.add_node(jid, node, ifrom)
+ self.get_node(jid, node, ifrom)['info'] = data
+
+ def get_cached_info(self, jid, node, ifrom, data):
+ """
+ Retrieve cached disco info data.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if isinstance(jid, JID):
+ jid = jid.full
+
+ if not self.node_exists(jid, node, ifrom):
+ return None
+ else:
+ return self.get_node(jid, node, ifrom)['info']
diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py
index c0c4d89d..feef5a13 100644
--- a/sleekxmpp/plugins/xep_0033.py
+++ b/sleekxmpp/plugins/xep_0033.py
@@ -7,155 +7,161 @@
"""
import logging
-from . import base
-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
+from sleekxmpp import Message
+from sleekxmpp.xmlstream.handler.callback import Callback
+from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from sleekxmpp.plugins import BasePlugin, register_plugin
class Addresses(ElementBase):
- namespace = 'http://jabber.org/protocol/address'
- name = 'addresses'
- plugin_attrib = 'addresses'
- interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
+ namespace = 'http://jabber.org/protocol/address'
+ name = 'addresses'
+ plugin_attrib = 'addresses'
+ interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
- def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False):
- address = Address(parent=self)
- address['type'] = atype
- address['jid'] = jid
- address['node'] = node
- address['uri'] = uri
- address['desc'] = desc
- address['delivered'] = delivered
- return address
+ def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False):
+ address = Address(parent=self)
+ address['type'] = atype
+ address['jid'] = jid
+ address['node'] = node
+ address['uri'] = uri
+ address['desc'] = desc
+ address['delivered'] = delivered
+ return address
- def getAddresses(self, atype=None):
- addresses = []
- for addrXML in self.xml.findall('{%s}address' % Address.namespace):
- # ElementTree 1.2.6 does not support [@attr='value'] in findall
- if atype is None or addrXML.attrib.get('type') == atype:
- addresses.append(Address(xml=addrXML, parent=None))
- return addresses
+ def getAddresses(self, atype=None):
+ addresses = []
+ for addrXML in self.xml.findall('{%s}address' % Address.namespace):
+ # ElementTree 1.2.6 does not support [@attr='value'] in findall
+ if atype is None or addrXML.attrib.get('type') == atype:
+ addresses.append(Address(xml=addrXML, parent=None))
+ return addresses
- def setAddresses(self, addresses, set_type=None):
- self.delAddresses(set_type)
- for addr in addresses:
- addr = dict(addr)
- # Remap 'type' to 'atype' to match the add method
- if set_type is not None:
- addr['type'] = set_type
- curr_type = addr.get('type', None)
- if curr_type is not None:
- del addr['type']
- addr['atype'] = curr_type
- self.addAddress(**addr)
+ def setAddresses(self, addresses, set_type=None):
+ self.delAddresses(set_type)
+ for addr in addresses:
+ addr = dict(addr)
+ # Remap 'type' to 'atype' to match the add method
+ if set_type is not None:
+ addr['type'] = set_type
+ curr_type = addr.get('type', None)
+ if curr_type is not None:
+ del addr['type']
+ addr['atype'] = curr_type
+ self.addAddress(**addr)
- def delAddresses(self, atype=None):
- if atype is None:
- return
- for addrXML in self.xml.findall('{%s}address' % Address.namespace):
- # ElementTree 1.2.6 does not support [@attr='value'] in findall
- if addrXML.attrib.get('type') == atype:
- self.xml.remove(addrXML)
+ def delAddresses(self, atype=None):
+ if atype is None:
+ return
+ for addrXML in self.xml.findall('{%s}address' % Address.namespace):
+ # ElementTree 1.2.6 does not support [@attr='value'] in findall
+ if addrXML.attrib.get('type') == atype:
+ self.xml.remove(addrXML)
- # --------------------------------------------------------------
+ # --------------------------------------------------------------
- def delBcc(self):
- self.delAddresses('bcc')
+ def delBcc(self):
+ self.delAddresses('bcc')
- def delCc(self):
- self.delAddresses('cc')
+ def delCc(self):
+ self.delAddresses('cc')
- def delNoreply(self):
- self.delAddresses('noreply')
+ def delNoreply(self):
+ self.delAddresses('noreply')
- def delReplyroom(self):
- self.delAddresses('replyroom')
+ def delReplyroom(self):
+ self.delAddresses('replyroom')
- def delReplyto(self):
- self.delAddresses('replyto')
+ def delReplyto(self):
+ self.delAddresses('replyto')
- def delTo(self):
- self.delAddresses('to')
+ def delTo(self):
+ self.delAddresses('to')
- # --------------------------------------------------------------
+ # --------------------------------------------------------------
- def getBcc(self):
- return self.getAddresses('bcc')
+ def getBcc(self):
+ return self.getAddresses('bcc')
- def getCc(self):
- return self.getAddresses('cc')
+ def getCc(self):
+ return self.getAddresses('cc')
- def getNoreply(self):
- return self.getAddresses('noreply')
+ def getNoreply(self):
+ return self.getAddresses('noreply')
- def getReplyroom(self):
- return self.getAddresses('replyroom')
+ def getReplyroom(self):
+ return self.getAddresses('replyroom')
- def getReplyto(self):
- return self.getAddresses('replyto')
+ def getReplyto(self):
+ return self.getAddresses('replyto')
- def getTo(self):
- return self.getAddresses('to')
+ def getTo(self):
+ return self.getAddresses('to')
- # --------------------------------------------------------------
+ # --------------------------------------------------------------
- def setBcc(self, addresses):
- self.setAddresses(addresses, 'bcc')
+ def setBcc(self, addresses):
+ self.setAddresses(addresses, 'bcc')
- def setCc(self, addresses):
- self.setAddresses(addresses, 'cc')
+ def setCc(self, addresses):
+ self.setAddresses(addresses, 'cc')
- def setNoreply(self, addresses):
- self.setAddresses(addresses, 'noreply')
+ def setNoreply(self, addresses):
+ self.setAddresses(addresses, 'noreply')
- def setReplyroom(self, addresses):
- self.setAddresses(addresses, 'replyroom')
+ def setReplyroom(self, addresses):
+ self.setAddresses(addresses, 'replyroom')
- def setReplyto(self, addresses):
- self.setAddresses(addresses, 'replyto')
+ def setReplyto(self, addresses):
+ self.setAddresses(addresses, 'replyto')
- def setTo(self, addresses):
- self.setAddresses(addresses, 'to')
+ def setTo(self, addresses):
+ self.setAddresses(addresses, 'to')
class Address(ElementBase):
- namespace = 'http://jabber.org/protocol/address'
- name = 'address'
- plugin_attrib = 'address'
- interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri'))
- address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
-
- def getDelivered(self):
- return self.xml.attrib.get('delivered', False)
-
- def setDelivered(self, delivered):
- if delivered:
- self.xml.attrib['delivered'] = "true"
- else:
- del self['delivered']
-
- def setUri(self, uri):
- if uri:
- del self['jid']
- del self['node']
- self.xml.attrib['uri'] = uri
- elif 'uri' in self.xml.attrib:
- del self.xml.attrib['uri']
-
-
-class xep_0033(base.base_plugin):
- """
- XEP-0033: Extended Stanza Addressing
- """
-
- def plugin_init(self):
- self.xep = '0033'
- self.description = 'Extended Stanza Addressing'
-
- registerStanzaPlugin(Message, Addresses)
-
- def post_init(self):
- base.base_plugin.post_init(self)
- self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace)
+ namespace = 'http://jabber.org/protocol/address'
+ name = 'address'
+ plugin_attrib = 'address'
+ interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri'))
+ address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
+
+ def getDelivered(self):
+ return self.xml.attrib.get('delivered', False)
+
+ def setDelivered(self, delivered):
+ if delivered:
+ self.xml.attrib['delivered'] = "true"
+ else:
+ del self['delivered']
+
+ def setUri(self, uri):
+ if uri:
+ del self['jid']
+ del self['node']
+ self.xml.attrib['uri'] = uri
+ elif 'uri' in self.xml.attrib:
+ del self.xml.attrib['uri']
+
+
+class XEP_0033(BasePlugin):
+
+ """
+ XEP-0033: Extended Stanza Addressing
+ """
+
+ name = 'xep_0033'
+ description = 'XEP-0033: Extended Stanza Addressing'
+ dependencies = set(['xep_0033'])
+
+ def plugin_init(self):
+ self.xep = '0033'
+
+ register_stanza_plugin(Message, Addresses)
+
+ self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace)
+
+
+xep_0033 = XEP_0033
+register_plugin(XEP_0033)
diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py
index ab3f750a..5035faae 100644
--- a/sleekxmpp/plugins/xep_0045.py
+++ b/sleekxmpp/plugins/xep_0045.py
@@ -6,14 +6,15 @@
See the file LICENSE for copying permission.
"""
from __future__ import with_statement
-from . import base
+
import logging
-from xml.etree import cElementTree as ET
-from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, JID
-from .. stanza.presence import Presence
-from .. xmlstream.handler.callback import Callback
-from .. xmlstream.matcher.xpath import MatchXPath
-from .. xmlstream.matcher.xmlmask import MatchXMLMask
+
+from sleekxmpp import Presence
+from sleekxmpp.plugins import BasePlugin, register_plugin
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET
+from sleekxmpp.xmlstream.handler.callback import Callback
+from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
+from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask
from sleekxmpp.exceptions import IqError, IqTimeout
@@ -107,18 +108,23 @@ class MUCPresence(ElementBase):
log.warning("Cannot delete room through mucpresence plugin.")
return self
-class xep_0045(base.base_plugin):
+
+class XEP_0045(BasePlugin):
+
"""
- Implements XEP-0045 Multi User Chat
+ Implements XEP-0045 Multi-User Chat
"""
+ name = 'xep_0045'
+ description = 'XEP-0045: Multi-User Chat'
+ dependencies = set(['xep_0030', 'xep_0004'])
+
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)
+ register_stanza_plugin(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))
@@ -374,3 +380,7 @@ class xep_0045(base.base_plugin):
if room not in self.rooms.keys():
return None
return self.rooms[room].keys()
+
+
+xep_0045 = XEP_0045
+register_plugin(XEP_0045)
diff --git a/sleekxmpp/plugins/xep_0047/__init__.py b/sleekxmpp/plugins/xep_0047/__init__.py
new file mode 100644
index 00000000..5cd7df2e
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0047/__init__.py
@@ -0,0 +1,21 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0047 import stanza
+from sleekxmpp.plugins.xep_0047.stanza import Open, Close, Data
+from sleekxmpp.plugins.xep_0047.stream import IBBytestream
+from sleekxmpp.plugins.xep_0047.ibb import XEP_0047
+
+
+register_plugin(XEP_0047)
+
+
+# Retain some backwards compatibility
+xep_0047 = XEP_0047
diff --git a/sleekxmpp/plugins/xep_0047/ibb.py b/sleekxmpp/plugins/xep_0047/ibb.py
new file mode 100644
index 00000000..c8a4b5e7
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0047/ibb.py
@@ -0,0 +1,148 @@
+import uuid
+import logging
+import threading
+
+from sleekxmpp import Message, Iq
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0047 import stanza, Open, Close, Data, IBBytestream
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0047(BasePlugin):
+
+ name = 'xep_0047'
+ description = 'XEP-0047: In-band Bytestreams'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.streams = {}
+ self.pending_streams = {3: 5}
+ self.pending_close_streams = {}
+ self._stream_lock = threading.Lock()
+
+ self.max_block_size = self.config.get('max_block_size', 8192)
+ self.window_size = self.config.get('window_size', 1)
+ self.auto_accept = self.config.get('auto_accept', True)
+ self.accept_stream = self.config.get('accept_stream', None)
+
+ register_stanza_plugin(Iq, Open)
+ register_stanza_plugin(Iq, Close)
+ register_stanza_plugin(Iq, Data)
+
+ self.xmpp.register_handler(Callback(
+ 'IBB Open',
+ StanzaPath('iq@type=set/ibb_open'),
+ self._handle_open_request))
+
+ self.xmpp.register_handler(Callback(
+ 'IBB Close',
+ StanzaPath('iq@type=set/ibb_close'),
+ self._handle_close))
+
+ self.xmpp.register_handler(Callback(
+ 'IBB Data',
+ StanzaPath('iq@type=set/ibb_data'),
+ self._handle_data))
+
+ self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb')
+
+ def _accept_stream(self, iq):
+ if self.accept_stream is not None:
+ return self.accept_stream(iq)
+ if self.auto_accept:
+ if iq['ibb_open']['block_size'] <= self.max_block_size:
+ return True
+ return False
+
+ def open_stream(self, jid, block_size=4096, sid=None, window=1,
+ ifrom=None, block=True, timeout=None, callback=None):
+ if sid is None:
+ sid = str(uuid.uuid4())
+
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['ibb_open']['block_size'] = block_size
+ iq['ibb_open']['sid'] = sid
+ iq['ibb_open']['stanza'] = 'iq'
+
+ stream = IBBytestream(self.xmpp, sid, block_size,
+ iq['to'], iq['from'], window)
+
+ with self._stream_lock:
+ self.pending_streams[iq['id']] = stream
+
+ self.pending_streams[iq['id']] = stream
+
+ if block:
+ resp = iq.send(timeout=timeout)
+ self._handle_opened_stream(resp)
+ return stream
+ else:
+ cb = None
+ if callback is not None:
+ def chained(resp):
+ self._handle_opened_stream(resp)
+ callback(resp)
+ cb = chained
+ else:
+ cb = self._handle_opened_stream
+ return iq.send(block=block, timeout=timeout, callback=cb)
+
+ def _handle_opened_stream(self, iq):
+ if iq['type'] == 'result':
+ with self._stream_lock:
+ stream = self.pending_streams.get(iq['id'], None)
+ if stream is not None:
+ stream.sender = iq['to']
+ stream.receiver = iq['from']
+ stream.stream_started.set()
+ self.streams[stream.sid] = stream
+ self.xmpp.event('ibb_stream_start', stream)
+
+ with self._stream_lock:
+ if iq['id'] in self.pending_streams:
+ del self.pending_streams[iq['id']]
+
+ def _handle_open_request(self, iq):
+ sid = iq['ibb_open']['sid']
+ size = iq['ibb_open']['block_size']
+ if not self._accept_stream(iq):
+ raise XMPPError('not-acceptable')
+
+ if size > self.max_block_size:
+ raise XMPPError('resource-constraint')
+
+ stream = IBBytestream(self.xmpp, sid, size,
+ iq['from'], iq['to'],
+ self.window_size)
+ stream.stream_started.set()
+ self.streams[sid] = stream
+ iq.reply()
+ iq.send()
+
+ self.xmpp.event('ibb_stream_start', stream)
+
+ def _handle_data(self, iq):
+ sid = iq['ibb_data']['sid']
+ stream = self.streams.get(sid, None)
+ if stream is not None and iq['from'] != stream.sender:
+ stream._recv_data(iq)
+ else:
+ raise XMPPError('item-not-found')
+
+ def _handle_close(self, iq):
+ sid = iq['ibb_close']['sid']
+ stream = self.streams.get(sid, None)
+ if stream is not None and iq['from'] != stream.sender:
+ stream._closed(iq)
+ else:
+ raise XMPPError('item-not-found')
diff --git a/sleekxmpp/plugins/xep_0047/stanza.py b/sleekxmpp/plugins/xep_0047/stanza.py
new file mode 100644
index 00000000..afba07a8
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0047/stanza.py
@@ -0,0 +1,67 @@
+import re
+import base64
+
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.xmlstream import ElementBase
+from sleekxmpp.thirdparty.suelta.util import bytes
+
+
+VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*')
+
+
+def to_b64(data):
+ return bytes(base64.b64encode(bytes(data))).decode('utf-8')
+
+
+def from_b64(data):
+ return bytes(base64.b64decode(bytes(data))).decode('utf-8')
+
+
+class Open(ElementBase):
+ name = 'open'
+ namespace = 'http://jabber.org/protocol/ibb'
+ plugin_attrib = 'ibb_open'
+ interfaces = set(('block_size', 'sid', 'stanza'))
+
+ def get_block_size(self):
+ return int(self._get_attr('block-size'))
+
+ def set_block_size(self, value):
+ self._set_attr('block-size', str(value))
+
+ def del_block_size(self):
+ self._del_attr('block-size')
+
+
+class Data(ElementBase):
+ name = 'data'
+ namespace = 'http://jabber.org/protocol/ibb'
+ plugin_attrib = 'ibb_data'
+ interfaces = set(('seq', 'sid', 'data'))
+ sub_interfaces = set(['data'])
+
+ def get_seq(self):
+ return int(self._get_attr('seq', '0'))
+
+ def set_seq(self, value):
+ self._set_attr('seq', str(value))
+
+ def get_data(self):
+ b64_data = self.xml.text.strip()
+ if VALID_B64.match(b64_data).group() == b64_data:
+ return from_b64(b64_data)
+ else:
+ raise XMPPError('not-acceptable')
+
+ def set_data(self, value):
+ self.xml.text = to_b64(value)
+
+ def del_data(self):
+ self.xml.text = ''
+
+
+class Close(ElementBase):
+ name = 'close'
+ namespace = 'http://jabber.org/protocol/ibb'
+ plugin_attrib = 'ibb_close'
+ interfaces = set(['sid'])
diff --git a/sleekxmpp/plugins/xep_0047/stream.py b/sleekxmpp/plugins/xep_0047/stream.py
new file mode 100644
index 00000000..49f56f36
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0047/stream.py
@@ -0,0 +1,137 @@
+import socket
+import threading
+import logging
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+from sleekxmpp.exceptions import XMPPError
+
+
+log = logging.getLogger(__name__)
+
+
+class IBBytestream(object):
+
+ def __init__(self, xmpp, sid, block_size, to, ifrom, window_size=1):
+ self.xmpp = xmpp
+ self.sid = sid
+ self.block_size = block_size
+ self.window_size = window_size
+
+ self.receiver = to
+ self.sender = ifrom
+
+ self.send_seq = -1
+ self.recv_seq = -1
+
+ self._send_seq_lock = threading.Lock()
+ self._recv_seq_lock = threading.Lock()
+
+ self.stream_started = threading.Event()
+ self.stream_in_closed = threading.Event()
+ self.stream_out_closed = threading.Event()
+
+ self.recv_queue = queue.Queue()
+
+ self.send_window = threading.BoundedSemaphore(value=self.window_size)
+ self.window_ids = set()
+ self.window_empty = threading.Event()
+ self.window_empty.set()
+
+ def send(self, data):
+ if not self.stream_started.is_set() or \
+ self.stream_out_closed.is_set():
+ raise socket.error
+ data = data[0:self.block_size]
+ self.send_window.acquire()
+ with self._send_seq_lock:
+ self.send_seq = (self.send_seq + 1) % 65535
+ seq = self.send_seq
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = self.receiver
+ iq['from'] = self.sender
+ iq['ibb_data']['sid'] = self.sid
+ iq['ibb_data']['seq'] = seq
+ iq['ibb_data']['data'] = data
+ self.window_empty.clear()
+ self.window_ids.add(iq['id'])
+ iq.send(block=False, callback=self._recv_ack)
+ return len(data)
+
+ def sendall(self, data):
+ sent_len = 0
+ while sent_len < len(data):
+ sent_len += self.send(data[sent_len:])
+
+ def _recv_ack(self, iq):
+ self.window_ids.remove(iq['id'])
+ if not self.window_ids:
+ self.window_empty.set()
+ self.send_window.release()
+ if iq['type'] == 'error':
+ self.close()
+
+ def _recv_data(self, iq):
+ with self._recv_seq_lock:
+ new_seq = iq['ibb_data']['seq']
+ if new_seq != (self.recv_seq + 1) % 65535:
+ self.close()
+ raise XMPPError('unexpected-request')
+ self.recv_seq = new_seq
+
+ data = iq['ibb_data']['data']
+ if len(data) > self.block_size:
+ self.close()
+ raise XMPPError('not-acceptable')
+
+ self.recv_queue.put(data)
+ self.xmpp.event('ibb_stream_data', {'stream': self, 'data': data})
+ iq.reply()
+ iq.send()
+
+ def recv(self, *args, **kwargs):
+ return self.read(block=True)
+
+ def read(self, block=True, timeout=None, **kwargs):
+ if not self.stream_started.is_set() or \
+ self.stream_in_closed.is_set():
+ raise socket.error
+ if timeout is not None:
+ block = True
+ try:
+ return self.recv_queue.get(block, timeout)
+ except:
+ return None
+
+ def close(self):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = self.receiver
+ iq['from'] = self.sender
+ iq['ibb_close']['sid'] = self.sid
+ self.stream_out_closed.set()
+ iq.send(block=False,
+ callback=lambda x: self.stream_in_closed.set())
+ self.xmpp.event('ibb_stream_end', self)
+
+ def _closed(self, iq):
+ self.stream_in_closed.set()
+ self.stream_out_closed.set()
+ while not self.window_empty.is_set():
+ log.info('waiting for send window to empty')
+ self.window_empty.wait(timeout=1)
+ iq.reply()
+ iq.send()
+ self.xmpp.event('ibb_stream_end', self)
+
+ def makefile(self, *args, **kwargs):
+ return self
+
+ def connect(*args, **kwargs):
+ return None
+
+ def shutdown(self, *args, **kwargs):
+ return None
diff --git a/sleekxmpp/plugins/xep_0050/__init__.py b/sleekxmpp/plugins/xep_0050/__init__.py
index 99f44f2a..640b182d 100644
--- a/sleekxmpp/plugins/xep_0050/__init__.py
+++ b/sleekxmpp/plugins/xep_0050/__init__.py
@@ -6,5 +6,14 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0050.stanza import Command
-from sleekxmpp.plugins.xep_0050.adhoc import xep_0050
+from sleekxmpp.plugins.xep_0050.adhoc import XEP_0050
+
+
+register_plugin(XEP_0050)
+
+
+# Retain some backwards compatibility
+xep_0050 = XEP_0050
diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py
index ec7b7041..8f2ea5c2 100644
--- a/sleekxmpp/plugins/xep_0050/adhoc.py
+++ b/sleekxmpp/plugins/xep_0050/adhoc.py
@@ -14,7 +14,7 @@ from sleekxmpp.exceptions import IqError
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin, JID
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0050 import stanza
from sleekxmpp.plugins.xep_0050 import Command
from sleekxmpp.plugins.xep_0004 import Form
@@ -23,7 +23,7 @@ from sleekxmpp.plugins.xep_0004 import Form
log = logging.getLogger(__name__)
-class xep_0050(base_plugin):
+class XEP_0050(BasePlugin):
"""
XEP-0050: Ad-Hoc Commands
@@ -78,12 +78,13 @@ class xep_0050(base_plugin):
terminate_command -- Command user API: delete a command's session
"""
+ name = 'xep_0050'
+ description = 'XEP-0050: Ad-Hoc Commands'
+ dependencies = set(['xep_0030', 'xep_0004'])
+ stanza = stanza
+
def plugin_init(self):
"""Start the XEP-0050 plugin."""
- self.xep = '0050'
- self.description = 'Ad-Hoc Commands'
- self.stanza = stanza
-
self.threaded = self.config.get('threaded', True)
self.commands = {}
self.sessions = self.config.get('session_db', {})
@@ -109,10 +110,8 @@ class xep_0050(base_plugin):
self._handle_command_complete,
threaded=self.threaded)
- def post_init(self):
- """Handle cross-plugin interactions."""
- base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature(Command.namespace)
+ self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple())
def set_backend(self, db):
"""
@@ -369,7 +368,6 @@ class xep_0050(base_plugin):
del self.sessions[sessionid]
-
# =================================================================
# Client side (command user) API
diff --git a/sleekxmpp/plugins/xep_0059/__init__.py b/sleekxmpp/plugins/xep_0059/__init__.py
index 3a9b8edf..3464ce32 100644
--- a/sleekxmpp/plugins/xep_0059/__init__.py
+++ b/sleekxmpp/plugins/xep_0059/__init__.py
@@ -6,5 +6,13 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0059.stanza import Set
-from sleekxmpp.plugins.xep_0059.rsm import ResultIterator, xep_0059
+from sleekxmpp.plugins.xep_0059.rsm import ResultIterator, XEP_0059
+
+
+register_plugin(XEP_0059)
+
+# Retain some backwards compatibility
+xep_0059 = XEP_0059
diff --git a/sleekxmpp/plugins/xep_0059/rsm.py b/sleekxmpp/plugins/xep_0059/rsm.py
index 35908473..9335ed22 100644
--- a/sleekxmpp/plugins/xep_0059/rsm.py
+++ b/sleekxmpp/plugins/xep_0059/rsm.py
@@ -10,9 +10,10 @@ import logging
import sleekxmpp
from sleekxmpp import Iq
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin, register_plugin
from sleekxmpp.xmlstream import register_stanza_plugin
-from sleekxmpp.plugins.xep_0059 import Set
+from sleekxmpp.plugins.xep_0059 import stanza, Set
+from sleekxmpp.exceptions import XMPPError
log = logging.getLogger(__name__)
@@ -70,38 +71,49 @@ class ResultIterator():
elif self.start:
self.query[self.interface]['rsm']['after'] = self.start
- r = self.query.send(block=True)
+ try:
+ r = self.query.send(block=True)
- if not r or not r[self.interface]['rsm']['first'] and \
- not r[self.interface]['rsm']['last']:
- raise StopIteration
+ if not r[self.interface]['rsm']['first'] and \
+ not r[self.interface]['rsm']['last']:
+ raise StopIteration
+
+ if r[self.interface]['rsm']['count'] and \
+ r[self.interface]['rsm']['first_index']:
+ count = int(r[self.interface]['rsm']['count'])
+ first = int(r[self.interface]['rsm']['first_index'])
+ num_items = len(r[self.interface]['substanzas'])
+ if first + num_items == count:
+ raise StopIteration
- if self.reverse:
- self.start = r[self.interface]['rsm']['first']
- else:
- self.start = r[self.interface]['rsm']['last']
+ if self.reverse:
+ self.start = r[self.interface]['rsm']['first']
+ else:
+ self.start = r[self.interface]['rsm']['last']
- return r
+ return r
+ except XMPPError:
+ raise StopIteration
-class xep_0059(base_plugin):
+class XEP_0059(BasePlugin):
"""
XEP-0050: Result Set Management
"""
+ name = 'xep_0059'
+ description = 'XEP-0059: Result Set Management'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
def plugin_init(self):
"""
Start the XEP-0059 plugin.
"""
- self.xep = '0059'
- self.description = 'Result Set Management'
- self.stanza = sleekxmpp.plugins.xep_0059.stanza
-
- def post_init(self):
- """Handle inter-plugin dependencies."""
- base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature(Set.namespace)
+ register_stanza_plugin(self.xmpp['xep_0030'].stanza.DiscoItems,
+ self.stanza.Set)
def iterate(self, stanza, interface):
"""
diff --git a/sleekxmpp/plugins/xep_0060/__init__.py b/sleekxmpp/plugins/xep_0060/__init__.py
index 026f7c2b..86e2f472 100644
--- a/sleekxmpp/plugins/xep_0060/__init__.py
+++ b/sleekxmpp/plugins/xep_0060/__init__.py
@@ -1,2 +1,19 @@
-from sleekxmpp.plugins.xep_0060.pubsub import xep_0060
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0060.pubsub import XEP_0060
from sleekxmpp.plugins.xep_0060 import stanza
+
+
+register_plugin(XEP_0060)
+
+
+# Retain some backwards compatibility
+xep_0060 = XEP_0060
diff --git a/sleekxmpp/plugins/xep_0060/pubsub.py b/sleekxmpp/plugins/xep_0060/pubsub.py
index 9e394ef2..31e59be9 100644
--- a/sleekxmpp/plugins/xep_0060/pubsub.py
+++ b/sleekxmpp/plugins/xep_0060/pubsub.py
@@ -9,23 +9,138 @@
import logging
from sleekxmpp.xmlstream import JID
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.plugins.base import BasePlugin
from sleekxmpp.plugins.xep_0060 import stanza
log = logging.getLogger(__name__)
-class xep_0060(base_plugin):
+class XEP_0060(BasePlugin):
"""
XEP-0060 Publish Subscribe
"""
+ name = 'xep_0060'
+ description = 'XEP-0060: Publish-Subscribe'
+ dependencies = set(['xep_0030', 'xep_0004'])
+ stanza = stanza
+
def plugin_init(self):
- self.xep = '0060'
- self.description = 'Publish-Subscribe'
- self.stanza = stanza
+ self.node_event_map = {}
+
+ self.xmpp.register_handler(
+ Callback('Pubsub Event: Items',
+ StanzaPath('message/pubsub_event/items'),
+ self._handle_event_items))
+ self.xmpp.register_handler(
+ Callback('Pubsub Event: Purge',
+ StanzaPath('message/pubsub_event/purge'),
+ self._handle_event_purge))
+ self.xmpp.register_handler(
+ Callback('Pubsub Event: Delete',
+ StanzaPath('message/pubsub_event/delete'),
+ self._handle_event_delete))
+ self.xmpp.register_handler(
+ Callback('Pubsub Event: Configuration',
+ StanzaPath('message/pubsub_event/configuration'),
+ self._handle_event_configuration))
+ self.xmpp.register_handler(
+ Callback('Pubsub Event: Subscription',
+ StanzaPath('message/pubsub_event/subscription'),
+ self._handle_event_subscription))
+
+ def _handle_event_items(self, msg):
+ """Raise events for publish and retraction notifications."""
+ node = msg['pubsub_event']['items']['node']
+
+ multi = len(msg['pubsub_event']['items']) > 1
+ values = {}
+ if multi:
+ values = msg.values
+ del values['pubsub_event']
+
+ for item in msg['pubsub_event']['items']:
+ event_name = self.node_event_map.get(node, None)
+ event_type = 'publish'
+ if item.name == 'retract':
+ event_type = 'retract'
+
+ if multi:
+ condensed = self.xmpp.Message()
+ condensed.values = values
+ condensed['pubsub_event']['items']['node'] = node
+ condensed['pubsub_event']['items'].append(item)
+ self.xmpp.event('pubsub_%s' % event_type, msg)
+ if event_name:
+ self.xmpp.event('%s_%s' % (event_name, event_type),
+ condensed)
+ else:
+ self.xmpp.event('pubsub_%s' % event_type, msg)
+ if event_name:
+ self.xmpp.event('%s_%s' % (event_name, event_type), msg)
+
+ def _handle_event_purge(self, msg):
+ """Raise events for node purge notifications."""
+ node = msg['pubsub_event']['purge']['node']
+ event_name = self.node_event_map.get(node, None)
+
+ self.xmpp.event('pubsub_purge', msg)
+ if event_name:
+ self.xmpp.event('%s_purge' % event_name, msg)
+
+ def _handle_event_delete(self, msg):
+ """Raise events for node deletion notifications."""
+ node = msg['pubsub_event']['delete']['node']
+ event_name = self.node_event_map.get(node, None)
+
+ self.xmpp.event('pubsub_delete', msg)
+ if event_name:
+ self.xmpp.event('%s_delete' % event_name, msg)
+
+ def _handle_event_configuration(self, msg):
+ """Raise events for node configuration notifications."""
+ node = msg['pubsub_event']['configuration']['node']
+ event_name = self.node_event_map.get(node, None)
+
+ self.xmpp.event('pubsub_config', msg)
+ if event_name:
+ self.xmpp.event('%s_config' % event_name, msg)
+
+ def _handle_event_subscription(self, msg):
+ """Raise events for node subscription notifications."""
+ node = msg['pubsub_event']['subscription']['node']
+ event_name = self.node_event_map.get(node, None)
+
+ self.xmpp.event('pubsub_subscription', msg)
+ if event_name:
+ self.xmpp.event('%s_subscription' % event_name, msg)
+
+ def map_node_event(self, node, event_name):
+ """
+ Map node names to events.
+
+ When a pubsub event is received for the given node,
+ raise the provided event.
+
+ For example::
+
+ map_node_event('http://jabber.org/protocol/tune',
+ 'user_tune')
+
+ will produce the events 'user_tune_publish' and 'user_tune_retract'
+ when the respective notifications are received from the node
+ 'http://jabber.org/protocol/tune', among other events.
+
+ Arguments:
+ node -- The node name to map to an event.
+ event_name -- The name of the event to raise when a
+ notification from the given node is received.
+ """
+ self.node_event_map[node] = event_name
def create_node(self, jid, node, config=None, ntype=None, ifrom=None,
block=True, callback=None, timeout=None):
@@ -98,8 +213,9 @@ class xep_0060(base_plugin):
ifrom -- Specify the sender's JID.
block -- Specify if the send call will block until a response
is received, or a timeout occurs. Defaults to True.
- timeout -- The length of time (in seconds) to wait for a response
- before exiting the send call if blocking is used.
+ timeout -- The length of time (in seconds) to wait for a
+ response before exiting the send call if blocking
+ is used.
Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
@@ -146,8 +262,9 @@ class xep_0060(base_plugin):
ifrom -- Specify the sender's JID.
block -- Specify if the send call will block until a response
is received, or a timeout occurs. Defaults to True.
- timeout -- The length of time (in seconds) to wait for a response
- before exiting the send call if blocking is used.
+ timeout -- The length of time (in seconds) to wait for a
+ response before exiting the send call if blocking
+ is used.
Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
@@ -183,8 +300,9 @@ class xep_0060(base_plugin):
iq['pubsub']['affiliations']['node'] = node
return iq.send(block=block, callback=callback, timeout=timeout)
- def get_subscription_options(self, jid, node=None, user_jid=None, ifrom=None,
- block=True, callback=None, timeout=None):
+ def get_subscription_options(self, jid, node=None, user_jid=None,
+ ifrom=None, block=True, callback=None,
+ timeout=None):
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
if user_jid is None:
iq['pubsub']['default']['node'] = node
@@ -364,7 +482,7 @@ class xep_0060(base_plugin):
"""
Discover the nodes provided by a Pubsub service, using disco.
"""
- return self.xmpp.plugin['xep_0030'].get_items(*args, **kwargs)
+ return self.xmpp['xep_0030'].get_items(*args, **kwargs)
def get_item(self, jid, node, item_id, ifrom=None, block=True,
callback=None, timeout=None):
@@ -372,7 +490,7 @@ class xep_0060(base_plugin):
Retrieve the content of an individual item.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
- item = self.stanza.Item()
+ item = stanza.Item()
item['id'] = item_id
iq['pubsub']['items']['node'] = node
iq['pubsub']['items'].append(item)
@@ -396,7 +514,7 @@ class xep_0060(base_plugin):
if item_ids is not None:
for item_id in item_ids:
- item = self.stanza.Item()
+ item = stanza.Item()
item['id'] = item_id
iq['pubsub']['items'].append(item)
@@ -410,12 +528,12 @@ class xep_0060(base_plugin):
"""
Retrieve the ItemIDs hosted by a given node, using disco.
"""
- return self.xmpp.plugin['xep_0030'].get_items(jid, node,
- ifrom=ifrom,
- block=block,
- callback=callback,
- timeout=timeout,
- iterator=iterator)
+ return self.xmpp['xep_0030'].get_items(jid, node,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout,
+ iterator=iterator)
def modify_affiliations(self, jid, node, affiliations=None, ifrom=None,
block=True, callback=None, timeout=None):
@@ -426,7 +544,7 @@ class xep_0060(base_plugin):
affiliations = []
for jid, affiliation in affiliations:
- aff = self.stanza.OwnerAffiliation()
+ aff = stanza.OwnerAffiliation()
aff['jid'] = jid
aff['affiliation'] = affiliation
iq['pubsub_owner']['affiliations'].append(aff)
@@ -442,7 +560,7 @@ class xep_0060(base_plugin):
subscriptions = []
for jid, subscription in subscriptions:
- sub = self.stanza.OwnerSubscription()
+ sub = stanza.OwnerSubscription()
sub['jid'] = jid
sub['subscription'] = subscription
iq['pubsub_owner']['subscriptions'].append(sub)
diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py
index c7263577..c0d4929e 100644
--- a/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py
+++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py
@@ -6,23 +6,26 @@
See the file LICENSE for copying permission.
"""
+import datetime as dt
+
from sleekxmpp import Message
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
from sleekxmpp.plugins.xep_0004 import Form
+from sleekxmpp.plugins import xep_0082
class Event(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'event'
plugin_attrib = 'pubsub_event'
- interfaces = set(('node',))
+ interfaces = set()
class EventItem(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'item'
plugin_attrib = name
- interfaces = set(('id', 'payload'))
+ interfaces = set(('id', 'payload', 'node', 'publisher'))
def set_payload(self, value):
self.xml.append(value)
@@ -76,7 +79,7 @@ class EventConfiguration(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'configuration'
plugin_attrib = name
- interfaces = set(('node', 'config'))
+ interfaces = set(('node',))
class EventPurge(ElementBase):
@@ -86,12 +89,47 @@ class EventPurge(ElementBase):
interfaces = set(('node',))
+class EventDelete(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'delete'
+ plugin_attrib = name
+ interfaces = set(('node', 'redirect'))
+
+ def set_redirect(self, uri):
+ del self['redirect']
+ redirect = ET.Element('{%s}redirect' % self.namespace)
+ redirect.attrib['uri'] = uri
+ self.xml.append(redirect)
+
+ def get_redirect(self):
+ redirect = self.xml.find('{%s}redirect' % self.namespace)
+ if redirect is not None:
+ return redirect.attrib.get('uri', '')
+ return ''
+
+ def del_redirect(self):
+ redirect = self.xml.find('{%s}redirect' % self.namespace)
+ if redirect is not None:
+ self.xml.remove(redirect)
+
+
class EventSubscription(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'subscription'
plugin_attrib = name
interfaces = set(('node', 'expiry', 'jid', 'subid', 'subscription'))
+ def get_expiry(self):
+ expiry = self._get_attr('expiry')
+ if expiry.lower() == 'presence':
+ return expiry
+ return xep_0082.parse(expiry)
+
+ def set_expiry(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self._set_attr('expiry', value)
+
def set_jid(self, value):
self._set_attr('jid', str(value))
@@ -102,8 +140,9 @@ class EventSubscription(ElementBase):
register_stanza_plugin(Message, Event)
register_stanza_plugin(Event, EventCollection)
register_stanza_plugin(Event, EventConfiguration)
-register_stanza_plugin(Event, EventItems)
register_stanza_plugin(Event, EventPurge)
+register_stanza_plugin(Event, EventDelete)
+register_stanza_plugin(Event, EventItems)
register_stanza_plugin(Event, EventSubscription)
register_stanza_plugin(EventCollection, EventAssociate)
register_stanza_plugin(EventCollection, EventDisassociate)
diff --git a/sleekxmpp/plugins/xep_0066/__init__.py b/sleekxmpp/plugins/xep_0066/__init__.py
index ebfbd0c2..68a50180 100644
--- a/sleekxmpp/plugins/xep_0066/__init__.py
+++ b/sleekxmpp/plugins/xep_0066/__init__.py
@@ -6,6 +6,15 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0066 import stanza
from sleekxmpp.plugins.xep_0066.stanza import OOB, OOBTransfer
-from sleekxmpp.plugins.xep_0066.oob import xep_0066
+from sleekxmpp.plugins.xep_0066.oob import XEP_0066
+
+
+register_plugin(XEP_0066)
+
+
+# Retain some backwards compatibility
+xep_0066 = XEP_0066
diff --git a/sleekxmpp/plugins/xep_0066/oob.py b/sleekxmpp/plugins/xep_0066/oob.py
index d1f4b3ff..dc215e83 100644
--- a/sleekxmpp/plugins/xep_0066/oob.py
+++ b/sleekxmpp/plugins/xep_0066/oob.py
@@ -13,19 +13,19 @@ from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0066 import stanza
log = logging.getLogger(__name__)
-class xep_0066(base_plugin):
+class XEP_0066(BasePlugin):
"""
- XEP-0066: Out-of-Band Data
+ XEP-0066: Out of Band Data
- Out-of-Band Data is a basic method for transferring files between
+ Out of Band Data is a basic method for transferring files between
XMPP agents. The URL of the resource in question is sent to the receiving
entity, which then downloads the resource before responding to the OOB
request. OOB is also used as a generic means to transmit URLs in other
@@ -42,11 +42,13 @@ class xep_0066(base_plugin):
or other addressable resource.
"""
+ name = 'xep_0066'
+ description = 'XEP-0066: Out of Band Data'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
def plugin_init(self):
"""Start the XEP-0066 plugin."""
- self.xep = '0066'
- self.description = 'Out-of-Band Transfer'
- self.stanza = stanza
self.url_handlers = {'global': self._default_handler,
'jid': {}}
@@ -60,9 +62,6 @@ class xep_0066(base_plugin):
StanzaPath('iq@type=set/oob_transfer'),
self._handle_transfer))
- def post_init(self):
- """Handle cross-plugin dependencies."""
- base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace)
self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace)
@@ -121,7 +120,7 @@ class xep_0066(base_plugin):
iq -- The Iq stanza containing the OOB transfer request.
"""
if iq['to'] in self.url_handlers['jid']:
- return self.url_handlers['jid'][jid](iq)
+ return self.url_handlers['jid'][iq['to']](iq)
else:
if self.url_handlers['global']:
self.url_handlers['global'](iq)
diff --git a/sleekxmpp/plugins/xep_0077/__init__.py b/sleekxmpp/plugins/xep_0077/__init__.py
new file mode 100644
index 00000000..779ae0ac
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0077/__init__.py
@@ -0,0 +1,19 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0077.stanza import Register, RegisterFeature
+from sleekxmpp.plugins.xep_0077.register import XEP_0077
+
+
+register_plugin(XEP_0077)
+
+
+# Retain some backwards compatibility
+xep_0077 = XEP_0077
diff --git a/sleekxmpp/plugins/xep_0077/register.py b/sleekxmpp/plugins/xep_0077/register.py
new file mode 100644
index 00000000..53cc9ef5
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0077/register.py
@@ -0,0 +1,90 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import StreamFeatures, Iq
+from sleekxmpp.xmlstream import register_stanza_plugin, JID
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0077 import stanza, Register, RegisterFeature
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0077(BasePlugin):
+
+ """
+ XEP-0077: In-Band Registration
+ """
+
+ name = 'xep_0077'
+ description = 'XEP-0077: In-Band Registration'
+ dependencies = set(['xep_0004', 'xep_0066'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.create_account = self.config.get('create_account', True)
+
+ register_stanza_plugin(StreamFeatures, RegisterFeature)
+ register_stanza_plugin(Iq, Register)
+
+ if self.xmpp.is_component:
+ pass
+ else:
+ self.xmpp.register_feature('register',
+ self._handle_register_feature,
+ restart=False,
+ order=self.config.get('order', 50))
+
+ register_stanza_plugin(Register, self.xmpp['xep_0004'].stanza.Form)
+ register_stanza_plugin(Register, self.xmpp['xep_0066'].stanza.OOB)
+
+ def _handle_register_feature(self, features):
+ if 'mechanisms' in self.xmpp.features:
+ # We have already logged in with an account
+ return False
+
+ if self.create_account:
+ form = self.get_registration()
+ self.xmpp.event('register', form, direct=True)
+ return True
+ return False
+
+ def get_registration(self, jid=None, ifrom=None, block=True,
+ timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq.enable('register')
+ return iq.send(block=block, timeout=timeout,
+ callback=callback, now=True)
+
+ def cancel_registration(self, jid=None, ifrom=None, block=True,
+ timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['register']['remove'] = True
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def change_password(self, password, jid=None, ifrom=None, block=True,
+ timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ if self.xmpp.is_component:
+ ifrom = JID(ifrom)
+ iq['register']['username'] = ifrom.user
+ else:
+ iq['register']['username'] = self.xmpp.boundjid.user
+ iq['register']['password'] = password
+ return iq.send(block=block, timeout=timeout, callback=callback)
diff --git a/sleekxmpp/plugins/xep_0077/stanza.py b/sleekxmpp/plugins/xep_0077/stanza.py
new file mode 100644
index 00000000..e06c1910
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0077/stanza.py
@@ -0,0 +1,73 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from __future__ import unicode_literals
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class Register(ElementBase):
+
+ namespace = 'jabber:iq:register'
+ name = 'query'
+ plugin_attrib = 'register'
+ interfaces = set(('username', 'password', 'email', 'nick', 'name',
+ 'first', 'last', 'address', 'city', 'state', 'zip',
+ 'phone', 'url', 'date', 'misc', 'text', 'key',
+ 'registered', 'remove', 'instructions', 'fields'))
+ sub_interfaces = interfaces
+ form_fields = set(('username', 'password', 'email', 'nick', 'name',
+ 'first', 'last', 'address', 'city', 'state', 'zip',
+ 'phone', 'url', 'date', 'misc', 'text', 'key'))
+
+ def get_registered(self):
+ present = self.xml.find('{%s}registered' % self.namespace)
+ return present is not None
+
+ def get_remove(self):
+ present = self.xml.find('{%s}remove' % self.namespace)
+ return present is not None
+
+ def set_registered(self, value):
+ if value:
+ self.add_field('registered')
+ else:
+ del self['registered']
+
+ def set_remove(self, value):
+ if value:
+ self.add_field('remove')
+ else:
+ del self['remove']
+
+ def add_field(self, value):
+ self._set_sub_text(value, '', keep=True)
+
+ def get_fields(self):
+ fields = set()
+ for field in self.form_fields:
+ if self.xml.find('{%s}%s' % (self.namespace, field)) is not None:
+ fields.add(field)
+ return fields
+
+ def set_fields(self, fields):
+ del self['fields']
+ for field in fields:
+ self._set_sub_text(field, '', keep=True)
+
+ def del_fields(self):
+ for field in self.form_fields:
+ self._del_sub(field)
+
+
+class RegisterFeature(ElementBase):
+
+ name = 'register'
+ namespace = 'http://jabber.org/features/iq-register'
+ plugin_attrib = name
+ interfaces = set()
diff --git a/sleekxmpp/plugins/xep_0078/__init__.py b/sleekxmpp/plugins/xep_0078/__init__.py
index 5a2bda77..2ea72ffb 100644
--- a/sleekxmpp/plugins/xep_0078/__init__.py
+++ b/sleekxmpp/plugins/xep_0078/__init__.py
@@ -6,7 +6,15 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0078 import stanza
from sleekxmpp.plugins.xep_0078.stanza import IqAuth, AuthFeature
-from sleekxmpp.plugins.xep_0078.legacyauth import xep_0078
+from sleekxmpp.plugins.xep_0078.legacyauth import XEP_0078
+
+
+register_plugin(XEP_0078)
+
+# Retain some backwards compatibility
+xep_0078 = XEP_0078
diff --git a/sleekxmpp/plugins/xep_0078/legacyauth.py b/sleekxmpp/plugins/xep_0078/legacyauth.py
index dec775a3..95587843 100644
--- a/sleekxmpp/plugins/xep_0078/legacyauth.py
+++ b/sleekxmpp/plugins/xep_0078/legacyauth.py
@@ -9,17 +9,19 @@
import logging
import hashlib
import random
+import sys
+from sleekxmpp.exceptions import IqError, IqTimeout
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0078 import stanza
log = logging.getLogger(__name__)
-class xep_0078(base_plugin):
+class XEP_0078(BasePlugin):
"""
XEP-0078 NON-SASL Authentication
@@ -28,11 +30,12 @@ class xep_0078(base_plugin):
unless you are forced to use an old XMPP server implementation.
"""
- def plugin_init(self):
- self.xep = "0078"
- self.description = "Non-SASL Authentication"
- self.stanza = stanza
+ name = 'xep_0078'
+ description = 'XEP-0078: Non-SASL Authentication'
+ dependencies = set()
+ stanza = stanza
+ def plugin_init(self):
self.xmpp.register_feature('auth',
self._handle_auth,
restart=False,
@@ -41,7 +44,6 @@ class xep_0078(base_plugin):
register_stanza_plugin(Iq, stanza.IqAuth)
register_stanza_plugin(StreamFeatures, stanza.AuthFeature)
-
def _handle_auth(self, features):
# If we can or have already authenticated with SASL, do nothing.
if 'mechanisms' in features['features']:
diff --git a/sleekxmpp/plugins/xep_0080/__init__.py b/sleekxmpp/plugins/xep_0080/__init__.py
new file mode 100644
index 00000000..cad23d22
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0080/__init__.py
@@ -0,0 +1,15 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0080.stanza import Geoloc
+from sleekxmpp.plugins.xep_0080.geoloc import XEP_0080
+
+
+register_plugin(XEP_0080)
diff --git a/sleekxmpp/plugins/xep_0080/geoloc.py b/sleekxmpp/plugins/xep_0080/geoloc.py
new file mode 100644
index 00000000..20dde4dd
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0080/geoloc.py
@@ -0,0 +1,122 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp.plugins.base import BasePlugin
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.xep_0080 import stanza, Geoloc
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0080(BasePlugin):
+
+ """
+ XEP-0080: User Location
+ """
+
+ name = 'xep_0080'
+ description = 'XEP-0080: User Location'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_init(self):
+ """Start the XEP-0080 plugin."""
+ self.xmpp['xep_0163'].register_pep('user_location', Geoloc)
+
+ def publish_location(self, **kwargs):
+ """
+ Publish the user's current location.
+
+ Arguments:
+ accuracy -- Horizontal GPS error in meters.
+ alt -- Altitude in meters above or below sea level.
+ area -- A named area such as a campus or neighborhood.
+ bearing -- GPS bearing (direction in which the entity is
+ heading to reach its next waypoint), measured in
+ decimal degrees relative to true north.
+ building -- A specific building on a street or in an area.
+ country -- The nation where the user is located.
+ countrycode -- The ISO 3166 two-letter country code.
+ datum -- GPS datum.
+ description -- A natural-language name for or description of
+ the location.
+ error -- Horizontal GPS error in arc minutes. Obsoleted by
+ the accuracy parameter.
+ floor -- A particular floor in a building.
+ lat -- Latitude in decimal degrees North.
+ locality -- A locality within the administrative region, such
+ as a town or city.
+ lon -- Longitude in decimal degrees East.
+ postalcode -- A code used for postal delivery.
+ region -- An administrative region of the nation, such
+ as a state or province.
+ room -- A particular room in a building.
+ speed -- The speed at which the entity is moving,
+ in meters per second.
+ street -- A thoroughfare within the locality, or a crossing
+ of two thoroughfares.
+ text -- A catch-all element that captures any other
+ information about the location.
+ timestamp -- UTC timestamp specifying the moment when the
+ reading was taken.
+ uri -- A URI or URL pointing to information about
+ the location.
+
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ options = kwargs.get('options', None)
+ ifrom = kwargs.get('ifrom', None)
+ block = kwargs.get('block', None)
+ callback = kwargs.get('callback', None)
+ timeout = kwargs.get('timeout', None)
+ for param in ('ifrom', 'block', 'callback', 'timeout', 'options'):
+ if param in kwargs:
+ del kwargs[param]
+
+ geoloc = Geoloc()
+ geoloc.values = kwargs
+
+ return self.xmpp['xep_0163'].publish(geoloc,
+ options=options,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
+
+ def stop(self, ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Clear existing user location information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ geoloc = Geoloc()
+ return self.xmpp['xep_0163'].publish(geoloc,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
diff --git a/sleekxmpp/plugins/xep_0080/stanza.py b/sleekxmpp/plugins/xep_0080/stanza.py
new file mode 100644
index 00000000..a83a8b1b
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0080/stanza.py
@@ -0,0 +1,266 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase
+from sleekxmpp.plugins import xep_0082
+
+
+class Geoloc(ElementBase):
+
+ """
+ XMPP's <geoloc> stanza allows entities to know the current
+ geographical or physical location of an entity. (XEP-0080: User Location)
+
+ Example <geoloc> stanzas:
+ <geoloc xmlns='http://jabber.org/protocol/geoloc'/>
+
+ <geoloc xmlns='http://jabber.org/protocol/geoloc' xml:lang='en'>
+ <accuracy>20</accuracy>
+ <country>Italy</country>
+ <lat>45.44</lat>
+ <locality>Venice</locality>
+ <lon>12.33</lon>
+ </geoloc>
+
+ Stanza Interface:
+ accuracy -- Horizontal GPS error in meters.
+ alt -- Altitude in meters above or below sea level.
+ area -- A named area such as a campus or neighborhood.
+ bearing -- GPS bearing (direction in which the entity is
+ heading to reach its next waypoint), measured in
+ decimal degrees relative to true north.
+ building -- A specific building on a street or in an area.
+ country -- The nation where the user is located.
+ countrycode -- The ISO 3166 two-letter country code.
+ datum -- GPS datum.
+ description -- A natural-language name for or description of
+ the location.
+ error -- Horizontal GPS error in arc minutes. Obsoleted by
+ the accuracy parameter.
+ floor -- A particular floor in a building.
+ lat -- Latitude in decimal degrees North.
+ locality -- A locality within the administrative region, such
+ as a town or city.
+ lon -- Longitude in decimal degrees East.
+ postalcode -- A code used for postal delivery.
+ region -- An administrative region of the nation, such
+ as a state or province.
+ room -- A particular room in a building.
+ speed -- The speed at which the entity is moving,
+ in meters per second.
+ street -- A thoroughfare within the locality, or a crossing
+ of two thoroughfares.
+ text -- A catch-all element that captures any other
+ information about the location.
+ timestamp -- UTC timestamp specifying the moment when the
+ reading was taken.
+ uri -- A URI or URL pointing to information about
+ the location.
+ """
+
+ namespace = 'http://jabber.org/protocol/geoloc'
+ name = 'geoloc'
+ interfaces = set(('accuracy', 'alt', 'area', 'bearing', 'building',
+ 'country', 'countrycode', 'datum', 'dscription',
+ 'error', 'floor', 'lat', 'locality', 'lon',
+ 'postalcode', 'region', 'room', 'speed', 'street',
+ 'text', 'timestamp', 'uri'))
+ sub_interfaces = interfaces
+ plugin_attrib = name
+
+ def exception(self, e):
+ """
+ Override exception passback for presence.
+ """
+ pass
+
+ def set_accuracy(self, accuracy):
+ """
+ Set the value of the <accuracy> element.
+
+ Arguments:
+ accuracy -- Horizontal GPS error in meters
+ """
+ self._set_sub_text('accuracy', text=str(accuracy))
+ return self
+
+ def get_accuracy(self):
+ """
+ Return the value of the <accuracy> element as an integer.
+ """
+ p = self._get_sub_text('accuracy')
+ if not p:
+ return None
+ else:
+ try:
+ return int(p)
+ except ValueError:
+ return None
+
+ def set_alt(self, alt):
+ """
+ Set the value of the <alt> element.
+
+ Arguments:
+ alt -- Altitude in meters above or below sea level
+ """
+ self._set_sub_text('alt', text=str(alt))
+ return self
+
+ def get_alt(self):
+ """
+ Return the value of the <alt> element as an integer.
+ """
+ p = self._get_sub_text('alt')
+ if not p:
+ return None
+ else:
+ try:
+ return int(p)
+ except ValueError:
+ return None
+
+ def set_bearing(self, bearing):
+ """
+ Set the value of the <bearing> element.
+
+ Arguments:
+ bearing -- GPS bearing (direction in which the entity is heading
+ to reach its next waypoint), measured in decimal
+ degrees relative to true north
+ """
+ self._set_sub_text('bearing', text=str(bearing))
+ return self
+
+ def get_bearing(self):
+ """
+ Return the value of the <bearing> element as a float.
+ """
+ p = self._get_sub_text('bearing')
+ if not p:
+ return None
+ else:
+ try:
+ return float(p)
+ except ValueError:
+ return None
+
+ def set_error(self, error):
+ """
+ Set the value of the <error> element.
+
+ Arguments:
+ error -- Horizontal GPS error in arc minutes; this
+ element is deprecated in favor of <accuracy/>
+ """
+ self._set_sub_text('error', text=str(error))
+ return self
+
+ def get_error(self):
+ """
+ Return the value of the <error> element as a float.
+ """
+ p = self._get_sub_text('error')
+ if not p:
+ return None
+ else:
+ try:
+ return float(p)
+ except ValueError:
+ return None
+
+ def set_lat(self, lat):
+ """
+ Set the value of the <lat> element.
+
+ Arguments:
+ lat -- Latitude in decimal degrees North
+ """
+ self._set_sub_text('lat', text=str(lat))
+ return self
+
+ def get_lat(self):
+ """
+ Return the value of the <lat> element as a float.
+ """
+ p = self._get_sub_text('lat')
+ if not p:
+ return None
+ else:
+ try:
+ return float(p)
+ except ValueError:
+ return None
+
+ def set_lon(self, lon):
+ """
+ Set the value of the <lon> element.
+
+ Arguments:
+ lon -- Longitude in decimal degrees East
+ """
+ self._set_sub_text('lon', text=str(lon))
+ return self
+
+ def get_lon(self):
+ """
+ Return the value of the <lon> element as a float.
+ """
+ p = self._get_sub_text('lon')
+ if not p:
+ return None
+ else:
+ try:
+ return float(p)
+ except ValueError:
+ return None
+
+ def set_speed(self, speed):
+ """
+ Set the value of the <speed> element.
+
+ Arguments:
+ speed -- The speed at which the entity is moving,
+ in meters per second
+ """
+ self._set_sub_text('speed', text=str(speed))
+ return self
+
+ def get_speed(self):
+ """
+ Return the value of the <speed> element as a float.
+ """
+ p = self._get_sub_text('speed')
+ if not p:
+ return None
+ else:
+ try:
+ return float(p)
+ except ValueError:
+ return None
+
+ def set_timestamp(self, timestamp):
+ """
+ Set the value of the <timestamp> element.
+
+ Arguments:
+ timestamp -- UTC timestamp specifying the moment when
+ the reading was taken
+ """
+ self._set_sub_text('timestamp', text=str(xep_0082.datetime(timestamp)))
+ return self
+
+ def get_timestamp(self):
+ """
+ Return the value of the <timestamp> element as a DateTime.
+ """
+ p = self._get_sub_text('timestamp')
+ if not p:
+ return None
+ else:
+ return xep_0082.datetime(p)
diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py
index 25c80fd0..96eb331a 100644
--- a/sleekxmpp/plugins/xep_0082.py
+++ b/sleekxmpp/plugins/xep_0082.py
@@ -9,7 +9,7 @@
import logging
import datetime as dt
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin, register_plugin
from sleekxmpp.thirdparty import tzutc, tzoffset, parse_iso
@@ -184,7 +184,8 @@ def datetime(year=None, month=None, day=None, hour=None,
return value
return format_datetime(value)
-class xep_0082(base_plugin):
+
+class XEP_0082(BasePlugin):
"""
XEP-0082: XMPP Date and Time Profiles
@@ -205,11 +206,12 @@ class xep_0082(base_plugin):
parse -- Convert a time string into a Python datetime object.
"""
+ name = 'xep_0082'
+ description = 'XEP-0082: XMPP Date and Time Profiles'
+ dependencies = set()
+
def plugin_init(self):
"""Start the XEP-0082 plugin."""
- self.xep = '0082'
- self.description = 'XMPP Date and Time Profiles'
-
self.date = date
self.datetime = datetime
self.time = time
@@ -217,3 +219,6 @@ class xep_0082(base_plugin):
self.format_datetime = format_datetime
self.format_time = format_time
self.parse = parse
+
+
+register_plugin(XEP_0082)
diff --git a/sleekxmpp/plugins/xep_0085/__init__.py b/sleekxmpp/plugins/xep_0085/__init__.py
index ff882f05..445d5059 100644
--- a/sleekxmpp/plugins/xep_0085/__init__.py
+++ b/sleekxmpp/plugins/xep_0085/__init__.py
@@ -6,5 +6,14 @@
See the file LICENSE for copying permissio
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0085.stanza import ChatState
-from sleekxmpp.plugins.xep_0085.chat_states import xep_0085
+from sleekxmpp.plugins.xep_0085.chat_states import XEP_0085
+
+
+register_plugin(XEP_0085)
+
+
+# Retain some backwards compatibility
+xep_0085 = XEP_0085
diff --git a/sleekxmpp/plugins/xep_0085/chat_states.py b/sleekxmpp/plugins/xep_0085/chat_states.py
index e95434d2..d10b317b 100644
--- a/sleekxmpp/plugins/xep_0085/chat_states.py
+++ b/sleekxmpp/plugins/xep_0085/chat_states.py
@@ -13,34 +13,36 @@ from sleekxmpp.stanza import Message
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0085 import stanza, ChatState
log = logging.getLogger(__name__)
-class xep_0085(base_plugin):
+class XEP_0085(BasePlugin):
"""
XEP-0085 Chat State Notifications
"""
- def plugin_init(self):
- self.xep = '0085'
- self.description = 'Chat State Notifications'
- self.stanza = stanza
+ name = 'xep_0085'
+ description = 'XEP-0085: Chat State Notifications'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
- for state in ChatState.states:
- self.xmpp.register_handler(
- Callback('Chat State: %s' % state,
- StanzaPath('message@chat_state=%s' % state),
- self._handle_chat_state))
+ def plugin_init(self):
+ self.xmpp.register_handler(
+ Callback('Chat State',
+ StanzaPath('message/chat_state'),
+ self._handle_chat_state))
- register_stanza_plugin(Message, ChatState)
+ register_stanza_plugin(Message, stanza.Active)
+ register_stanza_plugin(Message, stanza.Composing)
+ register_stanza_plugin(Message, stanza.Gone)
+ register_stanza_plugin(Message, stanza.Inactive)
+ register_stanza_plugin(Message, stanza.Paused)
- def post_init(self):
- base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace)
def _handle_chat_state(self, msg):
diff --git a/sleekxmpp/plugins/xep_0085/stanza.py b/sleekxmpp/plugins/xep_0085/stanza.py
index 8c46758c..c2cafb19 100644
--- a/sleekxmpp/plugins/xep_0085/stanza.py
+++ b/sleekxmpp/plugins/xep_0085/stanza.py
@@ -38,6 +38,7 @@ class ChatState(ElementBase):
namespace = 'http://jabber.org/protocol/chatstates'
plugin_attrib = 'chat_state'
interfaces = set(('chat_state',))
+ sub_interfaces = interfaces
is_extension = True
states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
@@ -71,3 +72,23 @@ class ChatState(ElementBase):
if state_xml is not None:
self.xml = ET.Element('')
parent.xml.remove(state_xml)
+
+
+class Active(ChatState):
+ name = 'active'
+
+
+class Composing(ChatState):
+ name = 'composing'
+
+
+class Gone(ChatState):
+ name = 'gone'
+
+
+class Inactive(ChatState):
+ name = 'inactive'
+
+
+class Paused(ChatState):
+ name = 'paused'
diff --git a/sleekxmpp/plugins/xep_0086/__init__.py b/sleekxmpp/plugins/xep_0086/__init__.py
index b021e2b5..94600e85 100644
--- a/sleekxmpp/plugins/xep_0086/__init__.py
+++ b/sleekxmpp/plugins/xep_0086/__init__.py
@@ -6,5 +6,14 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0086.stanza import LegacyError
-from sleekxmpp.plugins.xep_0086.legacy_error import xep_0086
+from sleekxmpp.plugins.xep_0086.legacy_error import XEP_0086
+
+
+register_plugin(XEP_0086)
+
+
+# Retain some backwards compatibility
+xep_0086 = XEP_0086
diff --git a/sleekxmpp/plugins/xep_0086/legacy_error.py b/sleekxmpp/plugins/xep_0086/legacy_error.py
index 25b98c5a..bed22ee2 100644
--- a/sleekxmpp/plugins/xep_0086/legacy_error.py
+++ b/sleekxmpp/plugins/xep_0086/legacy_error.py
@@ -8,11 +8,11 @@
from sleekxmpp.stanza import Error
from sleekxmpp.xmlstream import register_stanza_plugin
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0086 import stanza, LegacyError
-class xep_0086(base_plugin):
+class XEP_0086(BasePlugin):
"""
XEP-0086: Error Condition Mappings
@@ -33,10 +33,11 @@ class xep_0086(base_plugin):
iq['error']['legacy']['condition'] = ...
"""
- def plugin_init(self):
- self.xep = '0086'
- self.description = 'Error Condition Mappings'
- self.stanza = stanza
+ name = 'xep_0086'
+ description = 'XEP-0086: Error Condition Mappings'
+ dependencies = set()
+ stanza = stanza
+ def plugin_init(self):
register_stanza_plugin(Error, LegacyError,
overrides=self.config.get('override', True))
diff --git a/sleekxmpp/plugins/xep_0092/__init__.py b/sleekxmpp/plugins/xep_0092/__init__.py
index 7c5bdb76..293eaae6 100644
--- a/sleekxmpp/plugins/xep_0092/__init__.py
+++ b/sleekxmpp/plugins/xep_0092/__init__.py
@@ -6,6 +6,15 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0092 import stanza
from sleekxmpp.plugins.xep_0092.stanza import Version
-from sleekxmpp.plugins.xep_0092.version import xep_0092
+from sleekxmpp.plugins.xep_0092.version import XEP_0092
+
+
+register_plugin(XEP_0092)
+
+
+# Retain some backwards compatibility
+xep_0092 = XEP_0092
diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py
index ba72a9c3..c6223c10 100644
--- a/sleekxmpp/plugins/xep_0092/version.py
+++ b/sleekxmpp/plugins/xep_0092/version.py
@@ -13,27 +13,28 @@ from sleekxmpp import Iq
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.plugins.base import base_plugin
-from sleekxmpp.plugins.xep_0092 import Version
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0092 import Version, stanza
log = logging.getLogger(__name__)
-class xep_0092(base_plugin):
+class XEP_0092(BasePlugin):
"""
XEP-0092: Software Version
"""
+ name = 'xep_0092'
+ description = 'XEP-0092: Software Version'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
def plugin_init(self):
"""
Start the XEP-0092 plugin.
"""
- self.xep = "0092"
- self.description = "Software Version"
- self.stanza = sleekxmpp.plugins.xep_0092.stanza
-
self.name = self.config.get('name', 'SleekXMPP')
self.version = self.config.get('version', sleekxmpp.__version__)
self.os = self.config.get('os', '')
@@ -47,11 +48,6 @@ class xep_0092(base_plugin):
register_stanza_plugin(Iq, Version)
- def post_init(self):
- """
- Handle cross-plugin dependencies.
- """
- base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version')
def _handle_version(self, iq):
diff --git a/sleekxmpp/plugins/xep_0107/__init__.py b/sleekxmpp/plugins/xep_0107/__init__.py
new file mode 100644
index 00000000..04302df8
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0107/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0107 import stanza
+from sleekxmpp.plugins.xep_0107.stanza import UserMood
+from sleekxmpp.plugins.xep_0107.user_mood import XEP_0107
+
+
+register_plugin(XEP_0107)
diff --git a/sleekxmpp/plugins/xep_0107/stanza.py b/sleekxmpp/plugins/xep_0107/stanza.py
new file mode 100644
index 00000000..2c5814ea
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0107/stanza.py
@@ -0,0 +1,55 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class UserMood(ElementBase):
+
+ name = 'mood'
+ namespace = 'http://jabber.org/protocol/mood'
+ plugin_attrib = 'mood'
+ interfaces = set(['value', 'text'])
+ sub_interfaces = set(['text'])
+ moods = set(['afraid', 'amazed', 'amorous', 'angry', 'annoyed', 'anxious',
+ 'aroused', 'ashamed', 'bored', 'brave', 'calm', 'cautious',
+ 'cold', 'confident', 'confused', 'contemplative', 'contented',
+ 'cranky', 'crazy', 'creative', 'curious', 'dejected',
+ 'depressed', 'disappointed', 'disgusted', 'dismayed',
+ 'distracted', 'embarrassed', 'envious', 'excited',
+ 'flirtatious', 'frustrated', 'grateful', 'grieving', 'grumpy',
+ 'guilty', 'happy', 'hopeful', 'hot', 'humbled', 'humiliated',
+ 'hungry', 'hurt', 'impressed', 'in_awe', 'in_love',
+ 'indignant', 'interested', 'intoxicated', 'invincible',
+ 'jealous', 'lonely', 'lost', 'lucky', 'mean', 'moody',
+ 'nervous', 'neutral', 'offended', 'outraged', 'playful',
+ 'proud', 'relaxed', 'relieved', 'remorseful', 'restless',
+ 'sad', 'sarcastic', 'satisfied', 'serious', 'shocked',
+ 'shy', 'sick', 'sleepy', 'spontaneous', 'stressed', 'strong',
+ 'surprised', 'thankful', 'thirsty', 'tired', 'undefined',
+ 'weak', 'worried'])
+
+ def set_value(self, value):
+ self.del_value()
+ if value in self.moods:
+ self._set_sub_text(value, '', keep=True)
+ else:
+ raise ValueError('Unknown mood value')
+
+ def get_value(self):
+ for child in self.xml:
+ if child.tag.startswith('{%s}' % self.namespace):
+ elem_name = child.tag.split('}')[-1]
+ if elem_name in self.moods:
+ return elem_name
+ return ''
+
+ def del_value(self):
+ curr_value = self.get_value()
+ if curr_value:
+ self._set_sub_text(curr_value, '', keep=False)
diff --git a/sleekxmpp/plugins/xep_0107/user_mood.py b/sleekxmpp/plugins/xep_0107/user_mood.py
new file mode 100644
index 00000000..11aaace4
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0107/user_mood.py
@@ -0,0 +1,87 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp import Message
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.plugins.base import BasePlugin
+from sleekxmpp.plugins.xep_0107 import stanza, UserMood
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0107(BasePlugin):
+
+ """
+ XEP-0107: User Mood
+ """
+
+ name = 'xep_0107'
+ description = 'XEP-0107: User Mood'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, UserMood)
+ self.xmpp['xep_0163'].register_pep('user_mood', UserMood)
+
+ def publish_mood(self, value=None, text=None, options=None,
+ ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Publish the user's current mood.
+
+ Arguments:
+ value -- The name of the mood to publish.
+ text -- Optional natural-language description or reason
+ for the mood.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ mood = UserMood()
+ mood['value'] = value
+ mood['text'] = text
+ return self.xmpp['xep_0163'].publish(mood,
+ node=UserMood.namespace,
+ options=options,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
+
+ def stop(self, ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Clear existing user mood information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ mood = UserMood()
+ return self.xmpp['xep_0163'].publish(mood,
+ node=UserMood.namespace,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
diff --git a/sleekxmpp/plugins/xep_0108/__init__.py b/sleekxmpp/plugins/xep_0108/__init__.py
new file mode 100644
index 00000000..34d45113
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0108/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0108 import stanza
+from sleekxmpp.plugins.xep_0108.stanza import UserActivity
+from sleekxmpp.plugins.xep_0108.user_activity import XEP_0108
+
+
+register_plugin(XEP_0108)
diff --git a/sleekxmpp/plugins/xep_0108/stanza.py b/sleekxmpp/plugins/xep_0108/stanza.py
new file mode 100644
index 00000000..4dc18f43
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0108/stanza.py
@@ -0,0 +1,83 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class UserActivity(ElementBase):
+
+ name = 'activity'
+ namespace = 'http://jabber.org/protocol/activity'
+ plugin_attrib = 'activity'
+ interfaces = set(['value', 'text'])
+ sub_interfaces = set(['text'])
+ general = set(['doing_chores', 'drinking', 'eating', 'exercising',
+ 'grooming', 'having_appointment', 'inactive', 'relaxing',
+ 'talking', 'traveling', 'undefined', 'working'])
+ specific = set(['at_the_spa', 'brushing_teeth', 'buying_groceries',
+ 'cleaning', 'coding', 'commuting', 'cooking', 'cycling',
+ 'dancing', 'day_off', 'doing_maintenance',
+ 'doing_the_dishes', 'doing_the_laundry', 'driving',
+ 'fishing', 'gaming', 'gardening', 'getting_a_haircut',
+ 'going_out', 'hanging_out', 'having_a_beer',
+ 'having_a_snack', 'having_breakfast', 'having_coffee',
+ 'having_dinner', 'having_lunch', 'having_tea', 'hiding',
+ 'hiking', 'in_a_car', 'in_a_meeting', 'in_real_life',
+ 'jogging', 'on_a_bus', 'on_a_plane', 'on_a_train',
+ 'on_a_trip', 'on_the_phone', 'on_vacation',
+ 'on_video_phone', 'other', 'partying', 'playing_sports',
+ 'praying', 'reading', 'rehearsing', 'running',
+ 'running_an_errand', 'scheduled_holiday', 'shaving',
+ 'shopping', 'skiing', 'sleeping', 'smoking',
+ 'socializing', 'studying', 'sunbathing', 'swimming',
+ 'taking_a_bath', 'taking_a_shower', 'thinking',
+ 'walking', 'walking_the_dog', 'watching_a_movie',
+ 'watching_tv', 'working_out', 'writing'])
+
+ def set_value(self, value):
+ self.del_value()
+ general = value
+ specific = None
+ if isinstance(value, tuple) or isinstance(value, list):
+ general = value[0]
+ specific = value[1]
+
+ if general in self.general:
+ gen_xml = ET.Element('{%s}%s' % (self.namespace, general))
+ if specific:
+ spec_xml = ET.Element('{%s}%s' % (self.namespace, specific))
+ if specific in self.specific:
+ gen_xml.append(spec_xml)
+ else:
+ raise ValueError('Unknown specific activity')
+ self.xml.append(gen_xml)
+ else:
+ raise ValueError('Unknown general activity')
+
+ def get_value(self):
+ general = None
+ specific = None
+ gen_xml = None
+ for child in self.xml:
+ if child.tag.startswith('{%s}' % self.namespace):
+ elem_name = child.tag.split('}')[-1]
+ if elem_name in self.general:
+ general = elem_name
+ gen_xml = child
+ if gen_xml is not None:
+ for child in gen_xml:
+ if child.tag.startswith('{%s}' % self.namespace):
+ elem_name = child.tag.split('}')[-1]
+ if elem_name in self.specific:
+ specific = elem_name
+ return (general, specific)
+
+ def del_value(self):
+ curr_value = self.get_value()
+ if curr_value[0]:
+ self._set_sub_text(curr_value[0], '', keep=False)
diff --git a/sleekxmpp/plugins/xep_0108/user_activity.py b/sleekxmpp/plugins/xep_0108/user_activity.py
new file mode 100644
index 00000000..43270486
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0108/user_activity.py
@@ -0,0 +1,84 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.plugins.base import BasePlugin
+from sleekxmpp.plugins.xep_0108 import stanza, UserActivity
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0108(BasePlugin):
+
+ """
+ XEP-0108: User Activity
+ """
+
+ name = 'xep_0108'
+ description = 'XEP-0108: User Activity'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp['xep_0163'].register_pep('user_activity', UserActivity)
+
+ def publish_activity(self, general, specific=None, text=None, options=None,
+ ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Publish the user's current activity.
+
+ Arguments:
+ general -- The required general category of the activity.
+ specific -- Optional specific activity being done as part
+ of the general category.
+ text -- Optional natural-language description or reason
+ for the activity.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ activity = UserActivity()
+ activity['value'] = (general, specific)
+ activity['text'] = text
+ return self.xmpp['xep_0163'].publish(activity,
+ node=UserActivity.namespace,
+ options=options,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
+
+ def stop(self, ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Clear existing user activity information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ activity = UserActivity()
+ return self.xmpp['xep_0163'].publish(activity,
+ node=UserActivity.namespace,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
diff --git a/sleekxmpp/plugins/xep_0115/__init__.py b/sleekxmpp/plugins/xep_0115/__init__.py
new file mode 100644
index 00000000..31a2c03a
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0115/__init__.py
@@ -0,0 +1,20 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0115.stanza import Capabilities
+from sleekxmpp.plugins.xep_0115.static import StaticCaps
+from sleekxmpp.plugins.xep_0115.caps import XEP_0115
+
+
+register_plugin(XEP_0115)
+
+
+# Retain some backwards compatibility
+xep_0115 = XEP_0115
diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py
new file mode 100644
index 00000000..3aa0f70f
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0115/caps.py
@@ -0,0 +1,305 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import hashlib
+import base64
+
+import sleekxmpp
+from sleekxmpp.stanza import StreamFeatures, Presence, Iq
+from sleekxmpp.xmlstream import register_stanza_plugin, JID
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0115 import stanza, StaticCaps
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0115(BasePlugin):
+
+ """
+ XEP-0115: Entity Capabalities
+ """
+
+ name = 'xep_0115'
+ description = 'XEP-0115: Entity Capabilities'
+ dependencies = set(['xep_0030', 'xep_0128', 'xep_0004'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.hashes = {'sha-1': hashlib.sha1,
+ 'md5': hashlib.md5}
+
+ self.hash = self.config.get('hash', 'sha-1')
+ self.caps_node = self.config.get('caps_node', None)
+ self.broadcast = self.config.get('broadcast', True)
+
+ if self.caps_node is None:
+ ver = sleekxmpp.__version__
+ self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver
+
+ register_stanza_plugin(Presence, stanza.Capabilities)
+ register_stanza_plugin(StreamFeatures, stanza.Capabilities)
+
+ self._disco_ops = ['cache_caps',
+ 'get_caps',
+ 'assign_verstring',
+ 'get_verstring',
+ 'supports',
+ 'has_identity']
+
+ self.xmpp.register_handler(
+ Callback('Entity Capabilites',
+ StanzaPath('presence/caps'),
+ self._handle_caps))
+
+ self.xmpp.add_filter('out', self._filter_add_caps)
+
+ self.xmpp.add_event_handler('entity_caps', self._process_caps,
+ threaded=True)
+
+ if not self.xmpp.is_component:
+ self.xmpp.register_feature('caps',
+ self._handle_caps_feature,
+ restart=False,
+ order=10010)
+
+ self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace)
+
+ disco = self.xmpp['xep_0030']
+ self.static = StaticCaps(self.xmpp, disco.static)
+
+ for op in self._disco_ops:
+ disco._add_disco_op(op, getattr(self.static, op))
+
+ self._run_node_handler = disco._run_node_handler
+
+ disco.cache_caps = self.cache_caps
+ disco.update_caps = self.update_caps
+ disco.assign_verstring = self.assign_verstring
+ disco.get_verstring = self.get_verstring
+
+ def _filter_add_caps(self, stanza):
+ if isinstance(stanza, Presence) and self.broadcast:
+ ver = self.get_verstring(stanza['from'])
+ if ver:
+ stanza['caps']['node'] = self.caps_node
+ stanza['caps']['hash'] = self.hash
+ stanza['caps']['ver'] = ver
+ return stanza
+
+ def _handle_caps(self, presence):
+ if not self.xmpp.is_component:
+ if presence['from'] == self.xmpp.boundjid:
+ return
+ self.xmpp.event('entity_caps', presence)
+
+ def _handle_caps_feature(self, features):
+ # We already have a method to process presence with
+ # caps, so wrap things up and use that.
+ p = Presence()
+ p['from'] = self.xmpp.boundjid.domain
+ p.append(features['caps'])
+ self.xmpp.features.add('caps')
+
+ self.xmpp.event('entity_caps', p)
+
+ def _process_caps(self, pres):
+ if not pres['caps']['hash']:
+ log.debug("Received unsupported legacy caps.")
+ self.xmpp.event('entity_caps_legacy', pres)
+ return
+
+ existing_verstring = self.get_verstring(pres['from'].full)
+ if str(existing_verstring) == str(pres['caps']['ver']):
+ return
+
+ if pres['caps']['hash'] not in self.hashes:
+ try:
+ log.debug("Unknown caps hash: %s", pres['caps']['hash'])
+ self.xmpp['xep_003'].get_info(jid=pres['from'].full)
+ return
+ except XMPPError:
+ return
+
+ log.debug("New caps verification string: %s", pres['caps']['ver'])
+ try:
+ caps = self.xmpp['xep_0030'].get_info(
+ jid=pres['from'].full,
+ node='%s#%s' % (pres['caps']['node'],
+ pres['caps']['ver']))
+
+ if self._validate_caps(caps['disco_info'],
+ pres['caps']['hash'],
+ pres['caps']['ver']):
+ self.assign_verstring(pres['from'], pres['caps']['ver'])
+ except XMPPError:
+ log.debug("Could not retrieve disco#info results for caps")
+
+ def _validate_caps(self, caps, hash, check_verstring):
+ # Check Identities
+ full_ids = caps.get_identities(dedupe=False)
+ deduped_ids = caps.get_identities()
+ if len(full_ids) != len(deduped_ids):
+ log.debug("Duplicate disco identities found, invalid for caps")
+ return False
+
+ # Check Features
+
+ full_features = caps.get_features(dedupe=False)
+ deduped_features = caps.get_features()
+ if len(full_features) != len(deduped_features):
+ log.debug("Duplicate disco features found, invalid for caps")
+ return False
+
+ # Check Forms
+ form_types = []
+ deduped_form_types = set()
+ for stanza in caps['substanzas']:
+ if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
+ if 'FORM_TYPE' in stanza['fields']:
+ f_type = tuple(stanza['fields']['FORM_TYPE']['value'])
+ form_types.append(f_type)
+ deduped_form_types.add(f_type)
+ if len(form_types) != len(deduped_form_types):
+ log.debug("Duplicated FORM_TYPE values, invalid for caps")
+ return False
+
+ if len(f_type) > 1:
+ deduped_type = set(f_type)
+ if len(f_type) != len(deduped_type):
+ log.debug("Extra FORM_TYPE data, invalid for caps")
+ return False
+
+ if stanza['fields']['FORM_TYPE']['type'] != 'hidden':
+ log.debug("Field FORM_TYPE type not 'hidden', ignoring form for caps")
+ caps.xml.remove(stanza.xml)
+ else:
+ log.debug("No FORM_TYPE found, ignoring form for caps")
+ caps.xml.remove(stanza.xml)
+
+ verstring = self.generate_verstring(caps, hash)
+ if verstring != check_verstring:
+ log.debug("Verification strings do not match: %s, %s" % (
+ verstring, check_verstring))
+ return False
+
+ self.cache_caps(verstring, caps)
+ return True
+
+ def generate_verstring(self, info, hash):
+ hash = self.hashes.get(hash, None)
+ if hash is None:
+ return None
+
+ S = ''
+
+ # Convert None to '' in the identities
+ def clean_identity(id):
+ return map(lambda i: i or '', id)
+ identities = map(clean_identity, info['identities'])
+
+ identities = sorted(('/'.join(i) for i in identities))
+ features = sorted(info['features'])
+
+ S += '<'.join(identities) + '<'
+ S += '<'.join(features) + '<'
+
+ form_types = {}
+
+ for stanza in info['substanzas']:
+ if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
+ if 'FORM_TYPE' in stanza['fields']:
+ f_type = stanza['values']['FORM_TYPE']
+ if len(f_type):
+ f_type = f_type[0]
+ if f_type not in form_types:
+ form_types[f_type] = []
+ form_types[f_type].append(stanza)
+
+ sorted_forms = sorted(form_types.keys())
+ for f_type in sorted_forms:
+ for form in form_types[f_type]:
+ S += '%s<' % f_type
+ fields = sorted(form['fields'].keys())
+ fields.remove('FORM_TYPE')
+ for field in fields:
+ S += '%s<' % field
+ vals = form['fields'][field].get_value(convert=False)
+ if vals is None:
+ S += '<'
+ else:
+ if not isinstance(vals, list):
+ vals = [vals]
+ S += '<'.join(sorted(vals)) + '<'
+
+ binary = hash(S.encode('utf8')).digest()
+ return base64.b64encode(binary).decode('utf-8')
+
+ def update_caps(self, jid=None, node=None):
+ try:
+ info = self.xmpp['xep_0030'].get_info(jid, node, local=True)
+ if isinstance(info, Iq):
+ info = info['disco_info']
+ ver = self.generate_verstring(info, self.hash)
+ self.xmpp['xep_0030'].set_info(
+ jid=jid,
+ node='%s#%s' % (self.caps_node, ver),
+ info=info)
+ self.cache_caps(ver, info)
+ self.assign_verstring(jid, ver)
+
+ if self.xmpp.session_started_event.is_set() and self.broadcast:
+ # Check if we've sent directed presence. If we haven't, we
+ # can just send a normal presence stanza. If we have, then
+ # we will send presence to each contact individually so
+ # that we don't clobber existing statuses.
+ directed = False
+ for contact in self.xmpp.roster[jid]:
+ if self.xmpp.roster[jid][contact].last_status is not None:
+ directed = True
+ if not directed:
+ self.xmpp.roster[jid].send_last_presence()
+ else:
+ for contact in self.xmpp.roster[jid]:
+ self.xmpp.roster[jid][contact].send_last_presence()
+ except XMPPError:
+ return
+
+ def get_verstring(self, jid=None):
+ if jid in ('', None):
+ jid = self.xmpp.boundjid.full
+ if isinstance(jid, JID):
+ jid = jid.full
+ return self._run_node_handler('get_verstring', jid)
+
+ def assign_verstring(self, jid=None, verstring=None):
+ if jid in (None, ''):
+ jid = self.xmpp.boundjid.full
+ if isinstance(jid, JID):
+ jid = jid.full
+ return self._run_node_handler('assign_verstring', jid,
+ data={'verstring': verstring})
+
+ def cache_caps(self, verstring=None, info=None):
+ data = {'verstring': verstring, 'info': info}
+ return self._run_node_handler('cache_caps', None, None, data=data)
+
+ def get_caps(self, jid=None, verstring=None):
+ if verstring is None:
+ if jid is not None:
+ verstring = self.get_verstring(jid)
+ else:
+ return None
+ if isinstance(jid, JID):
+ jid = jid.full
+ data = {'verstring': verstring}
+ return self._run_node_handler('get_caps', jid, None, None, data)
diff --git a/sleekxmpp/plugins/xep_0115/stanza.py b/sleekxmpp/plugins/xep_0115/stanza.py
new file mode 100644
index 00000000..3e80b5cf
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0115/stanza.py
@@ -0,0 +1,19 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from __future__ import unicode_literals
+
+from sleekxmpp.xmlstream import ElementBase
+
+
+class Capabilities(ElementBase):
+
+ namespace = 'http://jabber.org/protocol/caps'
+ name = 'c'
+ plugin_attrib = 'caps'
+ interfaces = set(('hash', 'node', 'ver', 'ext'))
diff --git a/sleekxmpp/plugins/xep_0115/static.py b/sleekxmpp/plugins/xep_0115/static.py
new file mode 100644
index 00000000..a0a8fb23
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0115/static.py
@@ -0,0 +1,146 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.xmlstream import JID
+from sleekxmpp.exceptions import IqError, IqTimeout
+
+
+log = logging.getLogger(__name__)
+
+
+class StaticCaps(object):
+
+ """
+ Extend the default StaticDisco implementation to provide
+ support for extended identity information.
+ """
+
+ def __init__(self, xmpp, static):
+ """
+ Augment the default XEP-0030 static handler object.
+
+ Arguments:
+ static -- The default static XEP-0030 handler object.
+ """
+ self.xmpp = xmpp
+ self.disco = self.xmpp['xep_0030']
+ self.caps = self.xmpp['xep_0115']
+ self.static = static
+ self.ver_cache = {}
+ self.jid_vers = {}
+
+ def supports(self, jid, node, ifrom, data):
+ """
+ Check if a JID supports a given feature.
+
+ The data parameter may provide:
+ feature -- The feature to check for support.
+ 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.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ feature = data.get('feature', None)
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ if not feature:
+ return False
+
+ if node in (None, ''):
+ info = self.caps.get_caps(jid)
+ if info and feature in info['features']:
+ return True
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ return feature in info['disco_info']['features']
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def has_identity(self, jid, node, ifrom, data):
+ """
+ Check if a JID has a given identity.
+
+ The data parameter may provide:
+ category -- The category of the identity to check.
+ itype -- The type of the identity to check.
+ lang -- The language of the identity to check.
+ 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.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ identity = (data.get('category', None),
+ data.get('itype', None),
+ data.get('lang', None))
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ trunc = lambda i: (i[0], i[1], i[2])
+
+ if node in (None, ''):
+ info = self.caps.get_caps(jid)
+ if info and identity in map(trunc, info['identities']):
+ return True
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ return identity in map(trunc, info['disco_info']['identities'])
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def cache_caps(self, jid, node, ifrom, data):
+ with self.static.lock:
+ verstring = data.get('verstring', None)
+ info = data.get('info', None)
+ if not verstring or not info:
+ return
+ self.ver_cache[verstring] = info
+
+ def assign_verstring(self, jid, node, ifrom, data):
+ with self.static.lock:
+ if isinstance(jid, JID):
+ jid = jid.full
+ self.jid_vers[jid] = data.get('verstring', None)
+
+ def get_verstring(self, jid, node, ifrom, data):
+ with self.static.lock:
+ return self.jid_vers.get(jid, None)
+
+ def get_caps(self, jid, node, ifrom, data):
+ with self.static.lock:
+ return self.ver_cache.get(data.get('verstring', None), None)
diff --git a/sleekxmpp/plugins/xep_0118/__init__.py b/sleekxmpp/plugins/xep_0118/__init__.py
new file mode 100644
index 00000000..565f7844
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0118/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0118 import stanza
+from sleekxmpp.plugins.xep_0118.stanza import UserTune
+from sleekxmpp.plugins.xep_0118.user_tune import XEP_0118
+
+
+register_plugin(XEP_0118)
diff --git a/sleekxmpp/plugins/xep_0118/stanza.py b/sleekxmpp/plugins/xep_0118/stanza.py
new file mode 100644
index 00000000..80e0358a
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0118/stanza.py
@@ -0,0 +1,25 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class UserTune(ElementBase):
+
+ name = 'tune'
+ namespace = 'http://jabber.org/protocol/tune'
+ plugin_attrib = 'tune'
+ interfaces = set(['artist', 'length', 'rating', 'source',
+ 'title', 'track', 'uri'])
+ sub_interfaces = interfaces
+
+ def set_length(self, value):
+ self._set_sub_text('length', str(value))
+
+ def set_rating(self, value):
+ self._set_sub_text('rating', str(value))
diff --git a/sleekxmpp/plugins/xep_0118/user_tune.py b/sleekxmpp/plugins/xep_0118/user_tune.py
new file mode 100644
index 00000000..c848eaa8
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0118/user_tune.py
@@ -0,0 +1,92 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.plugins.base import BasePlugin
+from sleekxmpp.plugins.xep_0118 import stanza, UserTune
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0118(BasePlugin):
+
+ """
+ XEP-0118: User Tune
+ """
+
+ name = 'xep_0118'
+ description = 'XEP-0118: User Tune'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp['xep_0163'].register_pep('user_tune', UserTune)
+
+ def publish_tune(self, artist=None, length=None, rating=None, source=None,
+ title=None, track=None, uri=None, options=None,
+ ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Publish the user's current tune.
+
+ Arguments:
+ artist -- The artist or performer of the song.
+ length -- The length of the song in seconds.
+ rating -- The user's rating of the song (from 1 to 10)
+ source -- The album name, website, or other source of the song.
+ title -- The title of the song.
+ track -- The song's track number, or other unique identifier.
+ uri -- A URL to more information about the song.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ tune = UserTune()
+ tune['artist'] = artist
+ tune['length'] = length
+ tune['rating'] = rating
+ tune['source'] = source
+ tune['title'] = title
+ tune['track'] = track
+ tune['uri'] = uri
+ return self.xmpp['xep_0163'].publish(tune,
+ node=UserTune.namespace,
+ options=options,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
+
+ def stop(self, ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Clear existing user tune information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ tune = UserTune()
+ return self.xmpp['xep_0163'].publish(tune,
+ node=UserTune.namespace,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
diff --git a/sleekxmpp/plugins/xep_0128/__init__.py b/sleekxmpp/plugins/xep_0128/__init__.py
index 3c6379a3..27c2cc33 100644
--- a/sleekxmpp/plugins/xep_0128/__init__.py
+++ b/sleekxmpp/plugins/xep_0128/__init__.py
@@ -6,5 +6,14 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0128.static import StaticExtendedDisco
-from sleekxmpp.plugins.xep_0128.extended_disco import xep_0128
+from sleekxmpp.plugins.xep_0128.extended_disco import XEP_0128
+
+
+register_plugin(XEP_0128)
+
+
+# Retain some backwards compatibility
+xep_0128 = XEP_0128
diff --git a/sleekxmpp/plugins/xep_0128/extended_disco.py b/sleekxmpp/plugins/xep_0128/extended_disco.py
index 63b3cfee..d49741de 100644
--- a/sleekxmpp/plugins/xep_0128/extended_disco.py
+++ b/sleekxmpp/plugins/xep_0128/extended_disco.py
@@ -11,13 +11,13 @@ import logging
import sleekxmpp
from sleekxmpp import Iq
from sleekxmpp.xmlstream import register_stanza_plugin
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0004 import Form
from sleekxmpp.plugins.xep_0030 import DiscoInfo
from sleekxmpp.plugins.xep_0128 import StaticExtendedDisco
-class xep_0128(base_plugin):
+class XEP_0128(BasePlugin):
"""
XEP-0128: Service Discovery Extensions
@@ -39,11 +39,12 @@ class xep_0128(base_plugin):
del_extended_info -- Remove all extensions from a disco#info result.
"""
+ name = 'xep_0128'
+ description = 'XEP-0128: Service Discovery Extensions'
+ dependencies = set(['xep_0030', 'xep_0004'])
+
def plugin_init(self):
"""Start the XEP-0128 plugin."""
- self.xep = '0128'
- self.description = 'Service Discovery Extensions'
-
self._disco_ops = ['set_extended_info',
'add_extended_info',
'del_extended_info']
@@ -52,7 +53,6 @@ class xep_0128(base_plugin):
def post_init(self):
"""Handle cross-plugin dependencies."""
- base_plugin.post_init(self)
self.disco = self.xmpp['xep_0030']
self.static = StaticExtendedDisco(self.disco.static)
@@ -76,7 +76,7 @@ class xep_0128(base_plugin):
as extended information, replacing any
existing extensions.
"""
- self.disco._run_node_handler('set_extended_info', jid, node, kwargs)
+ self.disco._run_node_handler('set_extended_info', jid, node, None, kwargs)
def add_extended_info(self, jid=None, node=None, **kwargs):
"""
@@ -88,7 +88,7 @@ class xep_0128(base_plugin):
data -- Either a form, or a list of forms to add
as extended information.
"""
- self.disco._run_node_handler('add_extended_info', jid, node, kwargs)
+ self.disco._run_node_handler('add_extended_info', jid, node, None, kwargs)
def del_extended_info(self, jid=None, node=None, **kwargs):
"""
@@ -98,4 +98,4 @@ class xep_0128(base_plugin):
jid -- The JID to modify.
node -- The node to modify.
"""
- self.disco._run_node_handler('del_extended_info', jid, node, kwargs)
+ self.disco._run_node_handler('del_extended_info', jid, node, None, kwargs)
diff --git a/sleekxmpp/plugins/xep_0128/static.py b/sleekxmpp/plugins/xep_0128/static.py
index 493d9370..427011c0 100644
--- a/sleekxmpp/plugins/xep_0128/static.py
+++ b/sleekxmpp/plugins/xep_0128/static.py
@@ -31,42 +31,43 @@ class StaticExtendedDisco(object):
"""
self.static = static
- def set_extended_info(self, jid, node, data):
+ def set_extended_info(self, jid, node, ifrom, data):
"""
Replace the extended identity data for a JID/node combination.
The data parameter may provide:
data -- Either a single data form, or a list of data forms.
"""
- self.del_extended_info(jid, node, data)
- self.add_extended_info(jid, node, data)
+ with self.static.lock:
+ self.del_extended_info(jid, node, ifrom, data)
+ self.add_extended_info(jid, node, ifrom, data)
- def add_extended_info(self, jid, node, data):
+ def add_extended_info(self, jid, node, ifrom, data):
"""
Add additional extended identity data for a JID/node combination.
The data parameter may provide:
data -- Either a single data form, or a list of data forms.
"""
- self.static.add_node(jid, node)
+ with self.static.lock:
+ self.static.add_node(jid, node)
- forms = data.get('data', [])
- if not isinstance(forms, list):
- forms = [forms]
+ forms = data.get('data', [])
+ if not isinstance(forms, list):
+ forms = [forms]
- for form in forms:
- self.static.nodes[(jid, node)]['info'].append(form)
+ info = self.static.get_node(jid, node)['info']
+ for form in forms:
+ info.append(form)
- def del_extended_info(self, jid, node, data):
+ def del_extended_info(self, jid, node, ifrom, data):
"""
Replace the extended identity data for a JID/node combination.
The data parameter is not used.
"""
- if (jid, node) not in self.static.nodes:
- return
-
- info = self.static.nodes[(jid, node)]['info']
-
- for form in info['substanza']:
- info.xml.remove(form.xml)
+ with self.static.lock:
+ if self.static.node_exists(jid, node):
+ info = self.static.get_node(jid, node)['info']
+ for form in info['substanza']:
+ info.xml.remove(form.xml)
diff --git a/sleekxmpp/plugins/xep_0163.py b/sleekxmpp/plugins/xep_0163.py
new file mode 100644
index 00000000..5a6df1c8
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0163.py
@@ -0,0 +1,120 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.base import BasePlugin, register_plugin
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0163(BasePlugin):
+
+ """
+ XEP-0163: Personal Eventing Protocol (PEP)
+ """
+
+ name = 'xep_0163'
+ description = 'XEP-0163: Personal Eventing Protocol (PEP)'
+ dependencies = set(['xep_0030', 'xep_0060', 'xep_0115'])
+
+ def register_pep(self, name, stanza):
+ """
+ Setup and configure events and stanza registration for
+ the given PEP stanza:
+
+ - Add disco feature for the PEP content.
+ - Register disco interest in the PEP content.
+ - Map events from the PEP content's namespace to the given name.
+
+ :param str name: The event name prefix to use for PEP events.
+ :param stanza: The stanza class for the PEP content.
+ """
+ pubsub_stanza = self.xmpp['xep_0060'].stanza
+ register_stanza_plugin(pubsub_stanza.EventItem, stanza)
+
+ self.add_interest(stanza.namespace)
+ self.xmpp['xep_0030'].add_feature(stanza.namespace)
+ self.xmpp['xep_0060'].map_node_event(stanza.namespace, name)
+
+ def add_interest(self, namespace, jid=None):
+ """
+ Mark an interest in a PEP subscription by including a disco
+ feature with the '+notify' extension.
+
+ Arguments:
+ namespace -- The base namespace to register as an interest, such
+ as 'http://jabber.org/protocol/tune'. This may also
+ be a list of such namespaces.
+ jid -- Optionally specify the JID.
+ """
+ if not isinstance(namespace, set) and not isinstance(namespace, list):
+ namespace = [namespace]
+
+ for ns in namespace:
+ self.xmpp['xep_0030'].add_feature('%s+notify' % ns,
+ jid=jid)
+ self.xmpp['xep_0115'].update_caps(jid)
+
+ def remove_interest(self, namespace, jid=None):
+ """
+ Mark an interest in a PEP subscription by including a disco
+ feature with the '+notify' extension.
+
+ Arguments:
+ namespace -- The base namespace to remove as an interest, such
+ as 'http://jabber.org/protocol/tune'. This may also
+ be a list of such namespaces.
+ jid -- Optionally specify the JID.
+ """
+ if not isinstance(namespace, set) and not isinstance(namespace, list):
+ namespace = [namespace]
+
+ for ns in namespace:
+ self.xmpp['xep_0030'].del_feature(jid=jid,
+ feature='%s+notify' % namespace)
+ self.xmpp['xep_0115'].update_caps(jid)
+
+ def publish(self, stanza, node=None, id=None, options=None, ifrom=None,
+ block=True, callback=None, timeout=None):
+ """
+ Publish a PEP update.
+
+ This is just a (very) thin wrapper around the XEP-0060 publish()
+ method to set the defaults expected by PEP.
+
+ Arguments:
+ stanza -- The PEP update stanza to publish.
+ node -- The node to publish the item to. If not specified,
+ the stanza's namespace will be used.
+ id -- Optionally specify the ID of the item.
+ options -- A form of publish options.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ if node is None:
+ node = stanza.namespace
+
+ return self.xmpp['xep_0060'].publish(ifrom, node,
+ payload=stanza.xml,
+ options=options,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
+
+
+register_plugin(XEP_0163)
diff --git a/sleekxmpp/plugins/xep_0172/__init__.py b/sleekxmpp/plugins/xep_0172/__init__.py
new file mode 100644
index 00000000..aa7b9f72
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0172/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0172 import stanza
+from sleekxmpp.plugins.xep_0172.stanza import UserNick
+from sleekxmpp.plugins.xep_0172.user_nick import XEP_0172
+
+
+register_plugin(XEP_0172)
diff --git a/sleekxmpp/plugins/xep_0172/stanza.py b/sleekxmpp/plugins/xep_0172/stanza.py
new file mode 100644
index 00000000..110c237b
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0172/stanza.py
@@ -0,0 +1,67 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class UserNick(ElementBase):
+
+ """
+ XEP-0172: User Nickname allows the addition of a <nick> element
+ in several stanza types, including <message> and <presence> stanzas.
+
+ The nickname contained in a <nick> should be the global, friendly or
+ informal name chosen by the owner of a bare JID. The <nick> element
+ may be included when establishing communications with new entities,
+ such as normal XMPP users or MUC services.
+
+ The nickname contained in a <nick> element will not necessarily be
+ the same as the nickname used in a MUC.
+
+ Example stanzas:
+ <message to="user@example.com">
+ <nick xmlns="http://jabber.org/nick/nick">The User</nick>
+ <body>...</body>
+ </message>
+
+ <presence to="otheruser@example.com" type="subscribe">
+ <nick xmlns="http://jabber.org/nick/nick">The User</nick>
+ </presence>
+
+ Stanza Interface:
+ nick -- A global, friendly or informal name chosen by a user.
+
+ Methods:
+ setup -- Overrides ElementBase.setup.
+ get_nick -- Return the nickname in the <nick> element.
+ set_nick -- Add a <nick> element with the given nickname.
+ del_nick -- Remove the <nick> element.
+ """
+
+ namespace = 'http://jabber.org/protocol/nick'
+ name = 'nick'
+ plugin_attrib = name
+ interfaces = set(('nick',))
+
+ def set_nick(self, nick):
+ """
+ Add a <nick> element with the given nickname.
+
+ Arguments:
+ nick -- A human readable, informal name.
+ """
+ self.xml.text = nick
+
+ def get_nick(self):
+ """Return the nickname in the <nick> element."""
+ return self.xml.text
+
+ def del_nick(self):
+ """Remove the <nick> element."""
+ if self.parent is not None:
+ self.parent().xml.remove(self.xml)
diff --git a/sleekxmpp/plugins/xep_0172/user_nick.py b/sleekxmpp/plugins/xep_0172/user_nick.py
new file mode 100644
index 00000000..c20c3583
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0172/user_nick.py
@@ -0,0 +1,86 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza.message import Message
+from sleekxmpp.stanza.presence import Presence
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.plugins.base import BasePlugin
+from sleekxmpp.plugins.xep_0172 import stanza, UserNick
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0172(BasePlugin):
+
+ """
+ XEP-0172: User Nickname
+ """
+
+ name = 'xep_0172'
+ description = 'XEP-0172: User Nickname'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, UserNick)
+ register_stanza_plugin(Presence, UserNick)
+ self.xmpp['xep_0163'].register_pep('user_nick', UserNick)
+
+ def publish_nick(self, nick=None, options=None, ifrom=None, block=True,
+ callback=None, timeout=None):
+ """
+ Publish the user's current nick.
+
+ Arguments:
+ nick -- The user nickname to publish.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ nickname = UserNick()
+ nickname['nick'] = nick
+ return self.xmpp['xep_0163'].publish(nickname,
+ node=UserNick.namespace,
+ options=options,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
+
+ def stop(self, ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Clear existing user nick information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ nick = UserNick()
+ return self.xmpp['xep_0163'].publish(nick,
+ node=UserNick.namespace,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
diff --git a/sleekxmpp/plugins/xep_0184/__init__.py b/sleekxmpp/plugins/xep_0184/__init__.py
new file mode 100644
index 00000000..4b129b6b
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0184/__init__.py
@@ -0,0 +1,19 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0184.stanza import Request, Received
+from sleekxmpp.plugins.xep_0184.receipt import XEP_0184
+
+
+register_plugin(XEP_0184)
+
+
+# Retain some backwards compatibility
+xep_0184 = XEP_0184
diff --git a/sleekxmpp/plugins/xep_0184/receipt.py b/sleekxmpp/plugins/xep_0184/receipt.py
new file mode 100644
index 00000000..c0086b03
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0184/receipt.py
@@ -0,0 +1,120 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import Message
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0184 import stanza, Request, Received
+
+
+class XEP_0184(BasePlugin):
+
+ """
+ XEP-0184: Message Delivery Receipts
+ """
+
+ name = 'xep_0184'
+ description = 'XEP-0184: Message Delivery Receipts'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ ack_types = ('normal', 'chat', 'headline')
+
+ def plugin_init(self):
+ self.auto_ack = self.config.get('auto_ack', True)
+ self.auto_request = self.config.get('auto_request', False)
+
+ register_stanza_plugin(Message, Request)
+ register_stanza_plugin(Message, Received)
+
+ self.xmpp.add_filter('out', self._filter_add_receipt_request)
+
+ self.xmpp.register_handler(
+ Callback('Message Receipt',
+ StanzaPath('message/receipt'),
+ self._handle_receipt_received))
+
+ self.xmpp.register_handler(
+ Callback('Message Receipt Request',
+ StanzaPath('message/request_receipt'),
+ self._handle_receipt_request))
+
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:receipts')
+
+ def ack(self, msg):
+ """
+ Acknowledge a message by sending a receipt.
+
+ Arguments:
+ msg -- The message to acknowledge.
+ """
+ ack = self.xmpp.Message()
+ ack['to'] = msg['from']
+ ack['from'] = msg['to']
+ ack['receipt'] = msg['id']
+ ack['id'] = self.xmpp.new_id()
+ ack.send()
+
+ def _handle_receipt_received(self, msg):
+ self.xmpp.event('receipt_received', msg)
+
+ def _handle_receipt_request(self, msg):
+ """
+ Auto-ack message receipt requests if ``self.auto_ack`` is ``True``.
+
+ Arguments:
+ msg -- The incoming message requesting a receipt.
+ """
+ if self.auto_ack:
+ if msg['type'] in self.ack_types:
+ if not msg['receipt']:
+ self.ack(msg)
+
+ def _filter_add_receipt_request(self, stanza):
+ """
+ Auto add receipt requests to outgoing messages, if:
+
+ - ``self.auto_request`` is set to ``True``
+ - The message is not for groupchat
+ - The message does not contain a receipt acknowledgment
+ - The recipient is a bare JID or, if a full JID, one
+ that has the ``urn:xmpp:receipts`` feature enabled
+
+ The disco cache is checked if a full JID is specified in
+ the outgoing message, which may mean a round-trip disco#info
+ delay for the first message sent to the JID if entity caps
+ are not used.
+ """
+
+ if not self.auto_request:
+ return stanza
+
+ if not isinstance(stanza, Message):
+ return stanza
+
+ if stanza['request_receipt']:
+ return stanza
+
+ if not stanza['type'] in self.ack_types:
+ return stanza
+
+ if stanza['receipt']:
+ return stanza
+
+ if stanza['to'].resource:
+ if not self.xmpp['xep_0030'].supports(stanza['to'],
+ feature='urn:xmpp:receipts',
+ cached=True):
+ return stanza
+
+ stanza['request_receipt'] = True
+ return stanza
diff --git a/sleekxmpp/plugins/xep_0184/stanza.py b/sleekxmpp/plugins/xep_0184/stanza.py
new file mode 100644
index 00000000..a7607035
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0184/stanza.py
@@ -0,0 +1,72 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.stanzabase import ElementBase, ET
+
+
+class Request(ElementBase):
+ namespace = 'urn:xmpp:receipts'
+ name = 'request'
+ plugin_attrib = 'request_receipt'
+ interfaces = set(('request_receipt',))
+ sub_interfaces = interfaces
+ is_extension = True
+
+ def setup(self, xml=None):
+ self.xml = ET.Element('')
+ return True
+
+ def set_request_receipt(self, val):
+ self.del_request_receipt()
+ if val:
+ parent = self.parent()
+ parent._set_sub_text("{%s}request" % self.namespace, keep=True)
+ if not parent['id']:
+ if parent.stream:
+ parent['id'] = parent.stream.new_id()
+
+ def get_request_receipt(self):
+ parent = self.parent()
+ if parent.find("{%s}request" % self.namespace) is not None:
+ return True
+ else:
+ return False
+
+ def del_request_receipt(self):
+ self.parent()._del_sub("{%s}request" % self.namespace)
+
+
+class Received(ElementBase):
+ namespace = 'urn:xmpp:receipts'
+ name = 'received'
+ plugin_attrib = 'receipt'
+ interfaces = set(['receipt'])
+ sub_interfaces = interfaces
+ is_extension = True
+
+ def setup(self, xml=None):
+ self.xml = ET.Element('')
+ return True
+
+ def set_receipt(self, value):
+ self.del_receipt()
+ if value:
+ parent = self.parent()
+ xml = ET.Element("{%s}received" % self.namespace)
+ xml.attrib['id'] = value
+ parent.append(xml)
+
+ def get_receipt(self):
+ parent = self.parent()
+ xml = parent.find("{%s}received" % self.namespace)
+ if xml is not None:
+ return xml.attrib.get('id', '')
+ return ''
+
+ def del_receipt(self):
+ self.parent()._del_sub('{%s}received' % self.namespace)
diff --git a/sleekxmpp/plugins/xep_0198/__init__.py b/sleekxmpp/plugins/xep_0198/__init__.py
new file mode 100644
index 00000000..db930347
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0198/__init__.py
@@ -0,0 +1,20 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0198.stanza import Enable, Enabled
+from sleekxmpp.plugins.xep_0198.stanza import Resume, Resumed
+from sleekxmpp.plugins.xep_0198.stanza import Failed
+from sleekxmpp.plugins.xep_0198.stanza import StreamManagement
+from sleekxmpp.plugins.xep_0198.stanza import Ack, RequestAck
+
+from sleekxmpp.plugins.xep_0198.stream_management import XEP_0198
+
+
+register_plugin(XEP_0198)
diff --git a/sleekxmpp/plugins/xep_0198/stanza.py b/sleekxmpp/plugins/xep_0198/stanza.py
new file mode 100644
index 00000000..5cf93436
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0198/stanza.py
@@ -0,0 +1,151 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Error
+from sleekxmpp.xmlstream import ElementBase, StanzaBase
+
+
+class Enable(StanzaBase):
+ name = 'enable'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set(['max', 'resume'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_resume(self):
+ return self._get_attr('resume', 'false').lower() in ('true', '1')
+
+ def set_resume(self, val):
+ self._del_attr('resume')
+ self._set_attr('resume', 'true' if val else 'false')
+
+
+class Enabled(StanzaBase):
+ name = 'enabled'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set(['id', 'location', 'max', 'resume'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_resume(self):
+ return self._get_attr('resume', 'false').lower() in ('true', '1')
+
+ def set_resume(self, val):
+ self._del_attr('resume')
+ self._set_attr('resume', 'true' if val else 'false')
+
+
+class Resume(StanzaBase):
+ name = 'resume'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set(['h', 'previd'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_h(self):
+ h = self._get_attr('h', None)
+ if h:
+ return int(h)
+ return None
+
+ def set_h(self, val):
+ self._set_attr('h', str(val))
+
+
+class Resumed(StanzaBase):
+ name = 'resumed'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set(['h', 'previd'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_h(self):
+ h = self._get_attr('h', None)
+ if h:
+ return int(h)
+ return None
+
+ def set_h(self, val):
+ self._set_attr('h', str(val))
+
+
+
+class Failed(StanzaBase, Error):
+ name = 'failed'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set()
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+
+class StreamManagement(ElementBase):
+ name = 'sm'
+ namespace = 'urn:xmpp:sm:3'
+ plugin_attrib = name
+ interfaces = set(['required', 'optional'])
+
+ def get_required(self):
+ return self.find('{%s}required' % self.namespace) is not None
+
+ def set_required(self, val):
+ self.del_required()
+ if val:
+ self._set_sub_text('required', '', keep=True)
+
+ def del_required(self):
+ self._del_sub('required')
+
+ def get_optional(self):
+ return self.find('{%s}optional' % self.namespace) is not None
+
+ def set_optional(self, val):
+ self.del_optional()
+ if val:
+ self._set_sub_text('optional', '', keep=True)
+
+ def del_optional(self):
+ self._del_sub('optional')
+
+
+class RequestAck(StanzaBase):
+ name = 'r'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set()
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+
+class Ack(StanzaBase):
+ name = 'a'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set(['h'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_h(self):
+ h = self._get_attr('h', None)
+ if h:
+ return int(h)
+ return None
+
+ def set_h(self, val):
+ self._set_attr('h', str(val))
diff --git a/sleekxmpp/plugins/xep_0198/stream_management.py b/sleekxmpp/plugins/xep_0198/stream_management.py
new file mode 100644
index 00000000..6ed1ea26
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0198/stream_management.py
@@ -0,0 +1,266 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import threading
+import collections
+
+from sleekxmpp.stanza import Message, Presence, Iq, StreamFeatures
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback, Waiter
+from sleekxmpp.xmlstream.matcher import MatchXPath, MatchMany
+from sleekxmpp.plugins.base import BasePlugin
+from sleekxmpp.plugins.xep_0198 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+MAX_SEQ = 2**32
+
+
+class XEP_0198(BasePlugin):
+
+ """
+ XEP-0198: Stream Management
+ """
+
+ name = 'xep_0198'
+ description = 'XEP-0198: Stream Management'
+ dependencies = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ """Start the XEP-0198 plugin."""
+
+ # Only enable stream management for non-components,
+ # since components do not yet perform feature negotiation.
+ if self.xmpp.is_component:
+ return
+
+ #: The stream management ID for the stream. Knowing this value is
+ #: required in order to do stream resumption.
+ self.sm_id = self.config.get('sm_id', None)
+
+ #: A counter of handled incoming stanzas, mod 2^32.
+ self.handled = self.config.get('handled', 0)
+
+ #: A counter of unacked outgoing stanzas, mod 2^32.
+ self.seq = self.config.get('seq', 0)
+
+ #: The last ack number received from the server.
+ self.last_ack = self.config.get('last_ack', 0)
+
+ #: The number of stanzas to wait between sending ack requests to
+ #: the server. Setting this to ``1`` will send an ack request after
+ #: every sent stanza. Defaults to ``5``.
+ self.window = self.config.get('window', 5)
+
+ #: Control whether or not the ability to resume the stream will be
+ #: requested when enabling stream management. Defaults to ``True``.
+ self.allow_resume = self.config.get('allow_resume', True)
+
+ self.enabled = threading.Event()
+ self.unacked_queue = collections.deque()
+
+ self.seq_lock = threading.Lock()
+ self.handled_lock = threading.Lock()
+ self.ack_lock = threading.Lock()
+
+ register_stanza_plugin(StreamFeatures, stanza.StreamManagement)
+ self.xmpp.register_stanza(stanza.Enable)
+ self.xmpp.register_stanza(stanza.Enabled)
+ self.xmpp.register_stanza(stanza.Resume)
+ self.xmpp.register_stanza(stanza.Resumed)
+ self.xmpp.register_stanza(stanza.Ack)
+ self.xmpp.register_stanza(stanza.RequestAck)
+
+ # Register the feature twice because it may be ordered two
+ # different ways: enabling after binding and resumption
+ # before binding.
+ self.xmpp.register_feature('sm',
+ self._handle_sm_feature,
+ restart=True,
+ order=self.config.get('order', 10100))
+ self.xmpp.register_feature('sm',
+ self._handle_sm_feature,
+ restart=True,
+ order=self.config.get('resume_order', 9000))
+
+ self.xmpp.register_handler(
+ Callback('Stream Management Enabled',
+ MatchXPath(stanza.Enabled.tag_name()),
+ self._handle_enabled,
+ instream=True))
+
+ self.xmpp.register_handler(
+ Callback('Stream Management Resumed',
+ MatchXPath(stanza.Resumed.tag_name()),
+ self._handle_resumed,
+ instream=True))
+
+ self.xmpp.register_handler(
+ Callback('Stream Management Failed',
+ MatchXPath(stanza.Failed.tag_name()),
+ self._handle_failed,
+ instream=True))
+
+ self.xmpp.register_handler(
+ Callback('Stream Management Ack',
+ MatchXPath(stanza.Ack.tag_name()),
+ self._handle_ack,
+ instream=True))
+
+ self.xmpp.register_handler(
+ Callback('Stream Management Request Ack',
+ MatchXPath(stanza.RequestAck.tag_name()),
+ self._handle_request_ack,
+ instream=True))
+
+ self.xmpp.add_filter('in', self._handle_incoming)
+ self.xmpp.add_filter('out_sync', self._handle_outgoing)
+
+ self.xmpp.add_event_handler('need_ack', self.request_ack)
+
+ def send_ack(self):
+ """Send the current ack count to the server."""
+ ack = stanza.Ack(self.xmpp)
+ with self.handled_lock:
+ ack['h'] = self.handled
+ ack.send()
+
+ def request_ack(self, e=None):
+ """Request an ack from the server."""
+ req = stanza.RequestAck(self.xmpp)
+ req.send()
+
+ def _handle_sm_feature(self, features):
+ """
+ Enable or resume stream management.
+
+ If no SM-ID is stored, and resource binding has taken place,
+ stream management will be enabled.
+
+ If an SM-ID is known, and the server allows resumption, the
+ previous stream will be resumed.
+ """
+ if 'stream_management' in self.xmpp.features:
+ # We've already negotiated stream management,
+ # so no need to do it again.
+ return False
+ if not self.sm_id:
+ if 'bind' in self.xmpp.features:
+ self.enabled.set()
+ enable = stanza.Enable(self.xmpp)
+ enable['resume'] = self.allow_resume
+ enable.send()
+ self.handled = 0
+ elif self.sm_id and self.allow_resume:
+ self.enabled.set()
+ resume = stanza.Resume(self.xmpp)
+ resume['h'] = self.handled
+ resume['previd'] = self.sm_id
+ resume.send(now=True)
+
+ # Wait for a response before allowing stream feature processing
+ # to continue. The actual result processing will be done in the
+ # _handle_resumed() or _handle_failed() methods.
+ waiter = Waiter('resumed_or_failed',
+ MatchMany([
+ MatchXPath(stanza.Resumed.tag_name()),
+ MatchXPath(stanza.Failed.tag_name())]))
+ self.xmpp.register_handler(waiter)
+ result = waiter.wait()
+ if result is not None and result.name == 'resumed':
+ return True
+ return False
+
+ def _handle_enabled(self, stanza):
+ """Save the SM-ID, if provided.
+
+ Raises an :term:`sm_enabled` event.
+ """
+ self.xmpp.features.add('stream_management')
+ if stanza['id']:
+ self.sm_id = stanza['id']
+ self.xmpp.event('sm_enabled', stanza)
+
+ def _handle_resumed(self, stanza):
+ """Finish resuming a stream by resending unacked stanzas.
+
+ Raises a :term:`session_resumed` event.
+ """
+ self.xmpp.features.add('stream_management')
+ self._handle_ack(stanza)
+ for id, stanza in self.unacked_queue:
+ self.xmpp.send(stanza, now=True, use_filters=False)
+ self.xmpp.session_started_event.set()
+ self.xmpp.event('session_resumed', stanza)
+
+ def _handle_failed(self, stanza):
+ """
+ Disable and reset any features used since stream management was
+ requested (tracked stanzas may have been sent during the interval
+ between the enable request and the enabled response).
+
+ Raises an :term:`sm_failed` event.
+ """
+ self.enabled.clear()
+ self.unacked_queue.clear()
+ self.xmpp.event('sm_failed', stanza)
+
+ def _handle_ack(self, ack):
+ """Process a server ack by freeing acked stanzas from the queue.
+
+ Raises a :term:`stanza_acked` event for each acked stanza.
+ """
+ if ack['h'] == self.last_ack:
+ return
+
+ with self.ack_lock:
+ num_acked = (ack['h'] - self.last_ack) % MAX_SEQ
+ log.debug("Ack: %s, Last Ack: %s, Num Acked: %s, Unacked: %s",
+ ack['h'],
+ self.last_ack,
+ num_acked,
+ len(self.unacked_queue))
+ for x in range(num_acked):
+ seq, stanza = self.unacked_queue.popleft()
+ self.xmpp.event('stanza_acked', stanza)
+ self.last_ack = ack['h']
+
+ def _handle_request_ack(self, req):
+ """Handle an ack request by sending an ack."""
+ self.send_ack()
+
+ def _handle_incoming(self, stanza):
+ """Increment the handled counter for each inbound stanza."""
+ if not self.enabled.is_set():
+ return stanza
+
+ if isinstance(stanza, (Message, Presence, Iq)):
+ with self.handled_lock:
+ # Sequence numbers are mod 2^32
+ self.handled = (self.handled + 1) % MAX_SEQ
+ return stanza
+
+ def _handle_outgoing(self, stanza):
+ """Store outgoing stanzas in a queue to be acked."""
+ if not self.enabled.is_set():
+ return stanza
+
+ if isinstance(stanza, (Message, Presence, Iq)):
+ seq = None
+ with self.seq_lock:
+ # Sequence numbers are mod 2^32
+ self.seq = (self.seq + 1) % MAX_SEQ
+ seq = self.seq
+ self.unacked_queue.append((seq, stanza))
+ if len(self.unacked_queue) > self.window:
+ self.xmpp.event('need_ack')
+ return stanza
diff --git a/sleekxmpp/plugins/xep_0199/__init__.py b/sleekxmpp/plugins/xep_0199/__init__.py
index 3444fe94..5231a5b5 100644
--- a/sleekxmpp/plugins/xep_0199/__init__.py
+++ b/sleekxmpp/plugins/xep_0199/__init__.py
@@ -6,5 +6,15 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0199.stanza import Ping
-from sleekxmpp.plugins.xep_0199.ping import xep_0199
+from sleekxmpp.plugins.xep_0199.ping import XEP_0199
+
+
+register_plugin(XEP_0199)
+
+
+# Backwards compatibility for names
+xep_0199 = XEP_0199
+xep_0199.sendPing = xep_0199.send_ping
diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py
index a0f60532..851e5ae5 100644
--- a/sleekxmpp/plugins/xep_0199/ping.py
+++ b/sleekxmpp/plugins/xep_0199/ping.py
@@ -15,14 +15,14 @@ from sleekxmpp.exceptions import IqError, IqTimeout
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream.handler import Callback
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0199 import stanza, Ping
log = logging.getLogger(__name__)
-class xep_0199(base_plugin):
+class XEP_0199(BasePlugin):
"""
XEP-0199: XMPP Ping
@@ -47,14 +47,15 @@ class xep_0199(base_plugin):
round trip time.
"""
+ name = 'xep_0199'
+ description = 'XEP-0199: XMPP Ping'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
def plugin_init(self):
"""
Start the XEP-0199 plugin.
"""
- self.description = 'XMPP Ping'
- self.xep = '0199'
- self.stanza = stanza
-
self.keepalive = self.config.get('keepalive', False)
self.frequency = float(self.config.get('frequency', 300))
self.timeout = self.config.get('timeout', 30)
@@ -73,9 +74,6 @@ class xep_0199(base_plugin):
self.xmpp.add_event_handler('session_end',
self._handle_session_end)
- def post_init(self):
- """Handle cross-plugin dependencies."""
- base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature(Ping.namespace)
def _handle_keepalive(self, event):
@@ -169,7 +167,3 @@ class xep_0199(base_plugin):
log.debug("Pong: %s %f", jid, delay)
return delay
-
-
-# Backwards compatibility for names
-xep_0199.sendPing = xep_0199.send_ping
diff --git a/sleekxmpp/plugins/xep_0202/__init__.py b/sleekxmpp/plugins/xep_0202/__init__.py
index a34b2376..cdab3665 100644
--- a/sleekxmpp/plugins/xep_0202/__init__.py
+++ b/sleekxmpp/plugins/xep_0202/__init__.py
@@ -6,7 +6,15 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0202 import stanza
from sleekxmpp.plugins.xep_0202.stanza import EntityTime
-from sleekxmpp.plugins.xep_0202.time import xep_0202
+from sleekxmpp.plugins.xep_0202.time import XEP_0202
+
+
+register_plugin(XEP_0202)
+
+
+# Retain some backwards compatibility
+xep_0202 = XEP_0202
diff --git a/sleekxmpp/plugins/xep_0202/time.py b/sleekxmpp/plugins/xep_0202/time.py
index 2c6faa4b..ca388c5b 100644
--- a/sleekxmpp/plugins/xep_0202/time.py
+++ b/sleekxmpp/plugins/xep_0202/time.py
@@ -12,7 +12,7 @@ from sleekxmpp.stanza.iq import Iq
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins import xep_0082
from sleekxmpp.plugins.xep_0202 import stanza
@@ -20,18 +20,19 @@ from sleekxmpp.plugins.xep_0202 import stanza
log = logging.getLogger(__name__)
-class xep_0202(base_plugin):
+class XEP_0202(BasePlugin):
"""
XEP-0202: Entity Time
"""
+ name = 'xep_0202'
+ description = 'XEP-0202: Entity Time'
+ dependencies = set(['xep_0030', 'xep_0082'])
+ stanza = stanza
+
def plugin_init(self):
"""Start the XEP-0203 plugin."""
- self.xep = '0202'
- self.description = 'Entity Time'
- self.stanza = stanza
-
self.tz_offset = self.config.get('tz_offset', 0)
# As a default, respond to time requests with the
@@ -48,12 +49,8 @@ class xep_0202(base_plugin):
self._handle_time_request))
register_stanza_plugin(Iq, stanza.EntityTime)
- def post_init(self):
- """Handle cross-plugin interactions."""
- base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature('urn:xmpp:time')
-
def _handle_time_request(self, iq):
"""
Respond to a request for the local time.
diff --git a/sleekxmpp/plugins/xep_0203/__init__.py b/sleekxmpp/plugins/xep_0203/__init__.py
index 445ccf37..d4d99a6c 100644
--- a/sleekxmpp/plugins/xep_0203/__init__.py
+++ b/sleekxmpp/plugins/xep_0203/__init__.py
@@ -6,7 +6,16 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0203 import stanza
from sleekxmpp.plugins.xep_0203.stanza import Delay
-from sleekxmpp.plugins.xep_0203.delay import xep_0203
+from sleekxmpp.plugins.xep_0203.delay import XEP_0203
+
+
+
+register_plugin(XEP_0203)
+
+# Retain some backwards compatibility
+xep_0203 = XEP_0203
diff --git a/sleekxmpp/plugins/xep_0203/delay.py b/sleekxmpp/plugins/xep_0203/delay.py
index 8ff14d18..31f31ce3 100644
--- a/sleekxmpp/plugins/xep_0203/delay.py
+++ b/sleekxmpp/plugins/xep_0203/delay.py
@@ -9,11 +9,11 @@
from sleekxmpp.stanza import Message, Presence
from sleekxmpp.xmlstream import register_stanza_plugin
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0203 import stanza
-class xep_0203(base_plugin):
+class XEP_0203(BasePlugin):
"""
XEP-0203: Delayed Delivery
@@ -26,11 +26,12 @@ class xep_0203(base_plugin):
Also see <http://www.xmpp.org/extensions/xep-0203.html>.
"""
+ name = 'xep_0203'
+ description = 'XEP-0203: Delayed Delivery'
+ dependencies = set()
+ stanza = stanza
+
def plugin_init(self):
"""Start the XEP-0203 plugin."""
- self.xep = '0203'
- self.description = 'Delayed Delivery'
- self.stanza = stanza
-
register_stanza_plugin(Message, stanza.Delay)
register_stanza_plugin(Presence, stanza.Delay)
diff --git a/sleekxmpp/plugins/xep_0224/__init__.py b/sleekxmpp/plugins/xep_0224/__init__.py
index 62f5bf82..1a9d2342 100644
--- a/sleekxmpp/plugins/xep_0224/__init__.py
+++ b/sleekxmpp/plugins/xep_0224/__init__.py
@@ -6,6 +6,15 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0224 import stanza
from sleekxmpp.plugins.xep_0224.stanza import Attention
-from sleekxmpp.plugins.xep_0224.attention import xep_0224
+from sleekxmpp.plugins.xep_0224.attention import XEP_0224
+
+
+register_plugin(XEP_0224)
+
+
+# Retain some backwards compatibility
+xep_0224 = XEP_0224
diff --git a/sleekxmpp/plugins/xep_0224/attention.py b/sleekxmpp/plugins/xep_0224/attention.py
index 4a3ff368..6eea5d9d 100644
--- a/sleekxmpp/plugins/xep_0224/attention.py
+++ b/sleekxmpp/plugins/xep_0224/attention.py
@@ -12,25 +12,26 @@ from sleekxmpp.stanza import Message
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0224 import stanza
log = logging.getLogger(__name__)
-class xep_0224(base_plugin):
+class XEP_0224(BasePlugin):
"""
XEP-0224: Attention
"""
+ name = 'xep_0224'
+ description = 'XEP-0224: Attention'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
def plugin_init(self):
"""Start the XEP-0224 plugin."""
- self.xep = '0224'
- self.description = 'Attention'
- self.stanza = stanza
-
register_stanza_plugin(Message, stanza.Attention)
self.xmpp.register_handler(
@@ -38,9 +39,6 @@ class xep_0224(base_plugin):
StanzaPath('message/attention'),
self._handle_attention))
- def post_init(self):
- """Handle cross-plugin dependencies."""
- base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature(stanza.Attention.namespace)
def request_attention(self, to, mfrom=None, mbody=''):
diff --git a/sleekxmpp/plugins/xep_0249/__init__.py b/sleekxmpp/plugins/xep_0249/__init__.py
index e88d87ac..b85f55ce 100644
--- a/sleekxmpp/plugins/xep_0249/__init__.py
+++ b/sleekxmpp/plugins/xep_0249/__init__.py
@@ -6,5 +6,14 @@
See the file LICENSE for copying permission.
"""
+from sleekxmpp.plugins.base import register_plugin
+
from sleekxmpp.plugins.xep_0249.stanza import Invite
-from sleekxmpp.plugins.xep_0249.invite import xep_0249
+from sleekxmpp.plugins.xep_0249.invite import XEP_0249
+
+
+register_plugin(XEP_0249)
+
+
+# Retain some backwards compatibility
+xep_0249 = XEP_0249
diff --git a/sleekxmpp/plugins/xep_0249/invite.py b/sleekxmpp/plugins/xep_0249/invite.py
index 95fcb37c..737684f5 100644
--- a/sleekxmpp/plugins/xep_0249/invite.py
+++ b/sleekxmpp/plugins/xep_0249/invite.py
@@ -10,27 +10,28 @@ import logging
import sleekxmpp
from sleekxmpp import Message
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.plugins.xep_0249 import Invite
+from sleekxmpp.plugins.xep_0249 import Invite, stanza
log = logging.getLogger(__name__)
-class xep_0249(base_plugin):
+class XEP_0249(BasePlugin):
"""
XEP-0249: Direct MUC Invitations
"""
- def plugin_init(self):
- self.xep = "0249"
- self.description = "Direct MUC Invitations"
- self.stanza = sleekxmpp.plugins.xep_0249.stanza
+ name = 'xep_0249'
+ description = 'XEP-0249: Direct MUC Invitations'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+ def plugin_init(self):
self.xmpp.register_handler(
Callback('Direct MUC Invitations',
StanzaPath('message/groupchat_invite'),
@@ -38,8 +39,6 @@ class xep_0249(base_plugin):
register_stanza_plugin(Message, Invite)
- def post_init(self):
- base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature(Invite.namespace)
def _handle_invite(self, msg):
diff --git a/sleekxmpp/roster/item.py b/sleekxmpp/roster/item.py
index 6f956b31..9cb278a4 100644
--- a/sleekxmpp/roster/item.py
+++ b/sleekxmpp/roster/item.py
@@ -134,17 +134,22 @@ class RosterItem(object):
'subscription': 'none',
'name': '',
'groups': []}
+
self._db_state = {}
self.load()
- def set_backend(self, db=None):
+ def set_backend(self, db=None, save=True):
"""
Set the datastore interface object for the roster item.
Arguments:
- db -- The new datastore interface.
+ db -- The new datastore interface.
+ save -- If True, save the existing state to the new
+ backend datastore. Defaults to True.
"""
self.db = db
+ if save:
+ self.save()
self.load()
def load(self):
@@ -167,16 +172,25 @@ class RosterItem(object):
return self._state
return None
- def save(self):
+ def save(self, remove=False):
"""
Save the item's state information to an external datastore,
if one has been provided.
+
+ Arguments:
+ remove -- If True, expunge the item from the datastore.
"""
self['subscription'] = self._subscription()
+ if remove:
+ self._state['removed'] = True
if self.db:
self.db.save(self.owner, self.jid,
self._state, self._db_state)
+ # Finally, remove the in-memory copy if needed.
+ if remove:
+ del self.xmpp.roster[self.owner][self.jid]
+
def __getitem__(self, key):
"""Return a state field's value."""
if key in self._state:
@@ -482,3 +496,6 @@ class RosterItem(object):
a roster reset request.
"""
self.resources = {}
+
+ def __repr__(self):
+ return repr(self._state)
diff --git a/sleekxmpp/roster/multi.py b/sleekxmpp/roster/multi.py
index ee56f2a8..6a60778b 100644
--- a/sleekxmpp/roster/multi.py
+++ b/sleekxmpp/roster/multi.py
@@ -9,7 +9,6 @@
from sleekxmpp.xmlstream import JID
from sleekxmpp.roster import RosterNode
-
class Roster(object):
"""
@@ -68,6 +67,8 @@ class Roster(object):
"""
if isinstance(key, JID):
key = key.bare
+ if key is None:
+ key = self.xmpp.boundjid.bare
if key not in self._rosters:
self.add(key)
self._rosters[key].auto_authorize = self.auto_authorize
@@ -94,18 +95,23 @@ class Roster(object):
if node not in self._rosters:
self._rosters[node] = RosterNode(self.xmpp, node, self.db)
- def set_backend(self, db=None):
+ def set_backend(self, db=None, save=True):
"""
Set the datastore interface object for the roster.
Arguments:
db -- The new datastore interface.
+ save -- If True, save the existing state to the new
+ backend datastore. Defaults to True.
"""
self.db = db
- for node in self.db.entries(None, {}):
+ existing_entries = set(self._rosters)
+ new_entries = set(self.db.entries(None, {}))
+
+ for node in existing_entries:
+ self._rosters[node].set_backend(db, save)
+ for node in new_entries - existing_entries:
self.add(node)
- for node in self._rosters:
- self._rosters[node].set_backend(db)
def reset(self):
"""
@@ -182,3 +188,6 @@ class Roster(object):
self._auto_subscribe = value
for node in self._rosters:
self._rosters[node].auto_subscribe = value
+
+ def __repr__(self):
+ return repr(self._rosters)
diff --git a/sleekxmpp/roster/single.py b/sleekxmpp/roster/single.py
index c2c7497d..518afebe 100644
--- a/sleekxmpp/roster/single.py
+++ b/sleekxmpp/roster/single.py
@@ -57,11 +57,28 @@ class RosterNode(object):
self.auto_authorize = True
self.auto_subscribe = True
self.last_status = None
+ self._version = ''
self._jids = {}
if self.db:
+ if hasattr(self.db, 'version'):
+ self._version = self.db.version(self.jid)
for jid in self.db.entries(self.jid):
self.add(jid)
+
+ @property
+ def version(self):
+ """Retrieve the roster's version ID."""
+ if self.db and hasattr(self.db, 'version'):
+ self._version = self.db.version(self.jid)
+ return self._version
+
+ @version.setter
+ def version(self, version):
+ """Set the roster's version ID."""
+ self._version = version
+ if self.db and hasattr(self.db, 'set_version'):
+ self.db.set_version(self.jid, version)
def __getitem__(self, key):
"""
@@ -75,6 +92,17 @@ class RosterNode(object):
self.add(key, save=True)
return self._jids[key]
+ def __delitem__(self, key):
+ """
+ Remove a roster item from the local storage.
+
+ To remove an item from the server, use the remove() method.
+ """
+ if isinstance(key, JID):
+ key = key.bare
+ if key in self._jids:
+ del self._jids[key]
+
def __len__(self):
"""Return the number of JIDs referenced by the roster."""
return len(self._jids)
@@ -101,18 +129,23 @@ class RosterNode(object):
"""Iterate over the roster items."""
return self._jids.__iter__()
- def set_backend(self, db=None):
+ def set_backend(self, db=None, save=True):
"""
Set the datastore interface object for the roster node.
Arguments:
db -- The new datastore interface.
+ save -- If True, save the existing state to the new
+ backend datastore. Defaults to True.
"""
self.db = db
- for jid in self.db.entries(self.jid):
+ existing_entries = set(self._jids)
+ new_entries = set(self.db.entries(self.jid, {}))
+
+ for jid in existing_entries:
+ self._jids[jid].set_backend(db, save)
+ for jid in new_entries - existing_entries:
self.add(jid)
- for jid in self._jids:
- self._jids[jid].set_backend(db)
def add(self, jid, name='', groups=None, afrom=False, ato=False,
pending_in=False, pending_out=False, whitelisted=False,
@@ -144,6 +177,9 @@ class RosterNode(object):
"""
if isinstance(jid, JID):
key = jid.bare
+ else:
+ key = jid
+
state = {'name': name,
'groups': groups or [],
'from': afrom,
@@ -152,11 +188,11 @@ class RosterNode(object):
'pending_out': pending_out,
'whitelisted': whitelisted,
'subscription': 'none'}
- self._jids[jid] = RosterItem(self.xmpp, jid, self.jid,
+ self._jids[key] = RosterItem(self.xmpp, jid, self.jid,
state=state, db=self.db,
roster=self)
if save:
- self._jids[jid].save()
+ self._jids[key].save()
def subscribe(self, jid):
"""
@@ -285,3 +321,17 @@ class RosterNode(object):
if not self.xmpp.sentpresence:
self.xmpp.event('sent_presence')
self.xmpp.sentpresence = True
+
+ def send_last_presence(self):
+ if self.last_status is None:
+ self.send_presence()
+ else:
+ pres = self.last_status
+ if self.xmpp.is_component:
+ pres['from'] = self.jid
+ else:
+ del pres['from']
+ pres.send()
+
+ def __repr__(self):
+ return repr(self._jids)
diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py
index d985f729..825287ad 100644
--- a/sleekxmpp/stanza/error.py
+++ b/sleekxmpp/stanza/error.py
@@ -6,7 +6,7 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+from sleekxmpp.xmlstream import ElementBase, ET
class Error(ElementBase):
diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py
index f05dad17..47d51b04 100644
--- a/sleekxmpp/stanza/iq.py
+++ b/sleekxmpp/stanza/iq.py
@@ -6,7 +6,6 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.stanza import Error
from sleekxmpp.stanza.rootstanza import RootStanza
from sleekxmpp.xmlstream import StanzaBase, ET
from sleekxmpp.xmlstream.handler import Waiter, Callback
diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py
index 19d4d9e2..407802bd 100644
--- a/sleekxmpp/stanza/message.py
+++ b/sleekxmpp/stanza/message.py
@@ -6,9 +6,8 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.stanza import Error
from sleekxmpp.stanza.rootstanza import RootStanza
-from sleekxmpp.xmlstream import StanzaBase, ET
+from sleekxmpp.xmlstream import StanzaBase
class Message(RootStanza):
diff --git a/sleekxmpp/stanza/nick.py b/sleekxmpp/stanza/nick.py
index 1e23d34f..0e9a5c2b 100644
--- a/sleekxmpp/stanza/nick.py
+++ b/sleekxmpp/stanza/nick.py
@@ -6,67 +6,12 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.stanza import Message, Presence
-from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
-
-
-class Nick(ElementBase):
-
- """
- XEP-0172: User Nickname allows the addition of a <nick> element
- in several stanza types, including <message> and <presence> stanzas.
-
- The nickname contained in a <nick> should be the global, friendly or
- informal name chosen by the owner of a bare JID. The <nick> element
- may be included when establishing communications with new entities,
- such as normal XMPP users or MUC services.
-
- The nickname contained in a <nick> element will not necessarily be
- the same as the nickname used in a MUC.
-
- Example stanzas:
- <message to="user@example.com">
- <nick xmlns="http://jabber.org/nick/nick">The User</nick>
- <body>...</body>
- </message>
-
- <presence to="otheruser@example.com" type="subscribe">
- <nick xmlns="http://jabber.org/nick/nick">The User</nick>
- </presence>
-
- Stanza Interface:
- nick -- A global, friendly or informal name chosen by a user.
-
- Methods:
- setup -- Overrides ElementBase.setup.
- get_nick -- Return the nickname in the <nick> element.
- set_nick -- Add a <nick> element with the given nickname.
- del_nick -- Remove the <nick> element.
- """
-
- namespace = 'http://jabber.org/protocol/nick'
- name = 'nick'
- plugin_attrib = name
- interfaces = set(('nick',))
-
- def set_nick(self, nick):
- """
- Add a <nick> element with the given nickname.
-
- Arguments:
- nick -- A human readable, informal name.
- """
- self.xml.text = nick
-
- def get_nick(self):
- """Return the nickname in the <nick> element."""
- return self.xml.text
-
- def del_nick(self):
- """Remove the <nick> element."""
- if self.parent is not None:
- self.parent().xml.remove(self.xml)
+# The nickname stanza has been moved to its own plugin, but the existing
+# references are kept for backwards compatibility.
+from sleekxmpp.stanza import Message, Presence
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.xep_0172 import UserNick as Nick
register_stanza_plugin(Message, Nick)
register_stanza_plugin(Presence, Nick)
diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py
index c8706233..f2dd0968 100644
--- a/sleekxmpp/stanza/presence.py
+++ b/sleekxmpp/stanza/presence.py
@@ -6,9 +6,8 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.stanza import Error
from sleekxmpp.stanza.rootstanza import RootStanza
-from sleekxmpp.xmlstream import StanzaBase, ET
+from sleekxmpp.xmlstream import StanzaBase
class Presence(RootStanza):
diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py
index 2ac47d8b..bb756acb 100644
--- a/sleekxmpp/stanza/rootstanza.py
+++ b/sleekxmpp/stanza/rootstanza.py
@@ -7,8 +7,6 @@
"""
import logging
-import traceback
-import sys
from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout
from sleekxmpp.stanza import Error
diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py
index 3fcdbebc..4788ba72 100644
--- a/sleekxmpp/stanza/roster.py
+++ b/sleekxmpp/stanza/roster.py
@@ -36,7 +36,30 @@ class Roster(ElementBase):
namespace = 'jabber:iq:roster'
name = 'query'
plugin_attrib = 'roster'
- interfaces = set(('items',))
+ interfaces = set(('items', 'ver'))
+
+ def get_ver(self):
+ """
+ Ensure handling an empty ver attribute propery.
+
+ The ver attribute is special in that the presence of the
+ attribute with an empty value is important for boostrapping
+ roster versioning.
+ """
+ return self.xml.attrib.get('ver', None)
+
+ def set_ver(self, ver):
+ """
+ Ensure handling an empty ver attribute propery.
+
+ The ver attribute is special in that the presence of the
+ attribute with an empty value is important for boostrapping
+ roster versioning.
+ """
+ if ver is not None:
+ self.xml.attrib['ver'] = ver
+ else:
+ del self.xml.attrib['ver']
def set_items(self, items):
"""
@@ -55,20 +78,10 @@ class Roster(ElementBase):
"""
self.del_items()
for jid in items:
- ijid = str(jid)
- item = ET.Element('{jabber:iq:roster}item', {'jid': ijid})
- if 'subscription' in items[jid]:
- item.attrib['subscription'] = items[jid]['subscription']
- if 'name' in items[jid]:
- name = items[jid]['name']
- if name is not None:
- item.attrib['name'] = name
- if 'groups' in items[jid]:
- for group in items[jid]['groups']:
- groupxml = ET.Element('{jabber:iq:roster}group')
- groupxml.text = group
- item.append(groupxml)
- self.xml.append(item)
+ item = RosterItem()
+ item.values = items[jid]
+ item['jid'] = jid
+ self.append(item)
return self
def get_items(self):
@@ -83,31 +96,58 @@ class Roster(ElementBase):
been assigned.
"""
items = {}
- itemsxml = self.xml.findall('{jabber:iq:roster}item')
- if itemsxml is not None:
- for itemxml in itemsxml:
- item = {}
- item['name'] = itemxml.get('name', '')
- item['subscription'] = itemxml.get('subscription', '')
- item['ask'] = itemxml.get('ask', '')
- item['approved'] = itemxml.get('approved', '')
- item['groups'] = []
- groupsxml = itemxml.findall('{jabber:iq:roster}group')
- if groupsxml is not None:
- for groupxml in groupsxml:
- item['groups'].append(groupxml.text)
- items[itemxml.get('jid')] = item
+ for item in self['substanzas']:
+ if isinstance(item, RosterItem):
+ items[item['jid']] = item.values
+ # Remove extra JID reference to keep everything
+ # backward compatible
+ del items[item['jid']]['jid']
return items
def del_items(self):
"""
Remove all <item> elements from the roster stanza.
"""
- for child in self.xml.getchildren():
- self.xml.remove(child)
+ for item in self['substanzas']:
+ if isinstance(item, RosterItem):
+ self.xml.remove(item.xml)
+
+
+class RosterItem(ElementBase):
+ namespace = 'jabber:iq:roster'
+ name = 'item'
+ plugin_attrib = 'item'
+ interfaces = set(('jid', 'name', 'subscription', 'ask',
+ 'approved', 'groups'))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid', ''))
+
+ def set_jid(self, jid):
+ self._set_attr('jid', str(jid))
+
+ def get_groups(self):
+ groups = []
+ for group in self.xml.findall('{%s}group' % self.namespace):
+ groups.append(group.text)
+ return groups
+
+ def set_groups(self, values):
+ self.del_groups()
+ for group in values:
+ group_xml = ET.Element('{%s}group' % self.namespace)
+ group_xml.text = group
+ self.xml.append(group_xml)
+
+ def del_groups(self):
+ for group in self.xml.findall('{%s}group' % self.namespace):
+ self.xmp.remove(group)
+
+
register_stanza_plugin(Iq, Roster)
+register_stanza_plugin(Roster, RosterItem, iterable=True)
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
diff --git a/sleekxmpp/stanza/stream_error.py b/sleekxmpp/stanza/stream_error.py
index cf59a7fa..5a6dac96 100644
--- a/sleekxmpp/stanza/stream_error.py
+++ b/sleekxmpp/stanza/stream_error.py
@@ -7,8 +7,7 @@
"""
from sleekxmpp.stanza.error import Error
-from sleekxmpp.xmlstream import StanzaBase, ElementBase, ET
-from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream import StanzaBase
class StreamError(Error, StanzaBase):
diff --git a/sleekxmpp/stanza/stream_features.py b/sleekxmpp/stanza/stream_features.py
index b800011f..9993c84a 100644
--- a/sleekxmpp/stanza/stream_features.py
+++ b/sleekxmpp/stanza/stream_features.py
@@ -6,8 +6,7 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
-from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream import StanzaBase
class StreamFeatures(StanzaBase):
diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py
index dd3df29a..364e5939 100644
--- a/sleekxmpp/test/sleektest.py
+++ b/sleekxmpp/test/sleektest.py
@@ -7,6 +7,7 @@
"""
import unittest
+from xml.parsers.expat import ExpatError
try:
import Queue as queue
except:
@@ -62,8 +63,9 @@ class SleekTest(unittest.TestCase):
try:
xml = ET.fromstring(xml_string)
return xml
- except SyntaxError as e:
- if 'unbound' in e.msg:
+ except (SyntaxError, ExpatError) as e:
+ msg = e.msg if hasattr(e, 'msg') else e.message
+ if 'unbound' in msg:
known_prefixes = {
'stream': 'http://etherx.jabber.org/streams'}
diff --git a/sleekxmpp/thirdparty/mini_dateutil.py b/sleekxmpp/thirdparty/mini_dateutil.py
index 6af5ffde..d0d3f2ea 100644
--- a/sleekxmpp/thirdparty/mini_dateutil.py
+++ b/sleekxmpp/thirdparty/mini_dateutil.py
@@ -67,6 +67,7 @@
import re
+import math
import datetime
@@ -240,12 +241,12 @@ except:
if frac != None:
# ok, fractions of hour?
if min == None:
- frac, min = _math.modf(frac * 60.0)
+ frac, min = math.modf(frac * 60.0)
min = int(min)
# fractions of second?
if s == None:
- frac, s = _math.modf(frac * 60.0)
+ frac, s = math.modf(frac * 60.0)
s = int(s)
# and extract microseconds...
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
index 5cb2ee3d..2044ff80 100644
--- a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
@@ -3,3 +3,6 @@ from sleekxmpp.thirdparty.suelta.mechanisms.plain import PLAIN
from sleekxmpp.thirdparty.suelta.mechanisms.cram_md5 import CRAM_MD5
from sleekxmpp.thirdparty.suelta.mechanisms.digest_md5 import DIGEST_MD5
from sleekxmpp.thirdparty.suelta.mechanisms.scram_hmac import SCRAM_HMAC
+from sleekxmpp.thirdparty.suelta.mechanisms.messenger_oauth2 import X_MESSENGER_OAUTH2
+from sleekxmpp.thirdparty.suelta.mechanisms.facebook_platform import X_FACEBOOK_PLATFORM
+from sleekxmpp.thirdparty.suelta.mechanisms.google_token import X_GOOGLE_TOKEN
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py
index ba44befe..e07bb883 100644
--- a/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py
@@ -33,7 +33,7 @@ class CRAM_MD5(Mechanism):
if 'savepass' not in self.values:
del self.values['password']
- def process(self, challenge):
+ def process(self, challenge=None):
"""
"""
if challenge is None:
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
index 5492c553..890f3e24 100644
--- a/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
@@ -1,8 +1,10 @@
import sys
import random
+import hmac
from sleekxmpp.thirdparty.suelta.util import hash, bytes, quote
+from sleekxmpp.thirdparty.suelta.util import num_to_bytes, bytes_to_num
from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py b/sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py
new file mode 100644
index 00000000..cb0f09d5
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py
@@ -0,0 +1,39 @@
+from sleekxmpp.thirdparty.suelta.util import bytes
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
+
+
+class X_FACEBOOK_PLATFORM(Mechanism):
+
+ def __init__(self, sasl, name):
+ super(X_FACEBOOK_PLATFORM, self).__init__(sasl, name)
+ self.check_values(['access_token', 'api_key'])
+
+ def process(self, challenge=None):
+ if challenge is not None:
+ values = {}
+ for kv in challenge.split(b'&'):
+ key, value = kv.split(b'=')
+ values[key] = value
+
+ resp_data = {
+ b'method': values[b'method'],
+ b'v': b'1.0',
+ b'call_id': b'1.0',
+ b'nonce': values[b'nonce'],
+ b'access_token': self.values['access_token'],
+ b'api_key': self.values['api_key']
+ }
+ resp = '&'.join(['%s=%s' % (k, v) for k, v in resp_data.items()])
+ return bytes(resp)
+ return b''
+
+ def okay(self):
+ return True
+
+register_mechanism('X-FACEBOOK-PLATFORM', 40, X_FACEBOOK_PLATFORM, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py b/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py
new file mode 100644
index 00000000..e641bb91
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py
@@ -0,0 +1,22 @@
+from sleekxmpp.thirdparty.suelta.util import bytes
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
+
+
+
+class X_GOOGLE_TOKEN(Mechanism):
+
+ def __init__(self, sasl, name):
+ super(X_GOOGLE_TOKEN, self).__init__(sasl, name)
+ self.check_values(['email', 'access_token'])
+
+ def process(self, challenge=None):
+ email = bytes(self.values['email'])
+ token = bytes(self.values['access_token'])
+ return b'\x00' + email + b'\x00' + token
+
+ def okay(self):
+ return True
+
+
+register_mechanism('X-GOOGLE-TOKEN', 3, X_GOOGLE_TOKEN, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py b/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py
new file mode 100644
index 00000000..f5b0ddec
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py
@@ -0,0 +1,17 @@
+from sleekxmpp.thirdparty.suelta.util import bytes
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+
+
+class X_MESSENGER_OAUTH2(Mechanism):
+
+ def __init__(self, sasl, name):
+ super(X_MESSENGER_OAUTH2, self).__init__(sasl, name)
+ self.check_values(['access_token'])
+
+ def process(self, challenge=None):
+ return bytes(self.values['access_token'])
+
+ def okay(self):
+ return True
+
+register_mechanism('X-MESSENGER-OAUTH2', 10, X_MESSENGER_OAUTH2, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/plain.py b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py
index ab17095e..accae54a 100644
--- a/sleekxmpp/thirdparty/suelta/mechanisms/plain.py
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py
@@ -58,4 +58,4 @@ class PLAIN(Mechanism):
return True
-register_mechanism('PLAIN', 1, PLAIN, use_hashes=False)
+register_mechanism('PLAIN', 5, PLAIN, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/saslprep.py b/sleekxmpp/thirdparty/suelta/saslprep.py
index fe58d58b..8022e1cd 100644
--- a/sleekxmpp/thirdparty/suelta/saslprep.py
+++ b/sleekxmpp/thirdparty/suelta/saslprep.py
@@ -16,7 +16,7 @@ def saslprep(text, strict=True):
if sys.version_info < (3, 0):
if type(text) == str:
- text = text.decode('us-ascii')
+ text = text.decode('utf-8')
# Mapping:
#
diff --git a/sleekxmpp/version.py b/sleekxmpp/version.py
index 037c6463..543932a5 100644
--- a/sleekxmpp/version.py
+++ b/sleekxmpp/version.py
@@ -9,5 +9,5 @@
# We don't want to have to import the entire library
# just to get the version info for setup.py
-__version__ = '1.0'
-__version_info__ = (1, 0, 0, '', 0)
+__version__ = '1.0.1dev'
+__version_info__ = (1, 0, 1, 'dev', 0)
diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py
index 01ff5d67..899df17c 100644
--- a/sleekxmpp/xmlstream/handler/waiter.py
+++ b/sleekxmpp/xmlstream/handler/waiter.py
@@ -15,7 +15,6 @@ try:
except ImportError:
import Queue as queue
-from sleekxmpp.xmlstream import StanzaBase
from sleekxmpp.xmlstream.handler.base import BaseHandler
diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py
index c91c8fb3..281bf4ee 100644
--- a/sleekxmpp/xmlstream/jid.py
+++ b/sleekxmpp/xmlstream/jid.py
@@ -139,3 +139,7 @@ class JID(object):
def __ne__(self, other):
"""Two JIDs are considered unequal if they are not equal."""
return not self == other
+
+ def __hash__(self):
+ """Hash a JID based on the string version of its full JID."""
+ return hash(self.full)
diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py
index 61c5332c..a4c0fda0 100644
--- a/sleekxmpp/xmlstream/matcher/stanzapath.py
+++ b/sleekxmpp/xmlstream/matcher/stanzapath.py
@@ -10,6 +10,7 @@
"""
from sleekxmpp.xmlstream.matcher.base import MatcherBase
+from sleekxmpp.xmlstream.stanzabase import fix_ns
class StanzaPath(MatcherBase):
@@ -18,8 +19,16 @@ class StanzaPath(MatcherBase):
The StanzaPath matcher selects stanzas that match a given "stanza path",
which is similar to a normal XPath except that it uses the interfaces and
plugins of the stanza instead of the actual, underlying XML.
+
+ :param criteria: Object to compare some aspect of a stanza against.
"""
+ def __init__(self, criteria):
+ self._criteria = fix_ns(criteria, split=True,
+ propagate_ns=False,
+ default_ns='jabber:client')
+ self._raw_criteria = criteria
+
def match(self, stanza):
"""
Compare a stanza against a "stanza path". A stanza path is similar to
@@ -31,4 +40,4 @@ class StanzaPath(MatcherBase):
:param stanza: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
stanza to compare against.
"""
- return stanza.match(self._criteria)
+ return stanza.match(self._criteria) or stanza.match(self._raw_criteria)
diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py
index 4a6f073f..8ec73164 100644
--- a/sleekxmpp/xmlstream/scheduler.py
+++ b/sleekxmpp/xmlstream/scheduler.py
@@ -161,7 +161,7 @@ class Scheduler(object):
else:
break
for task in cleanup:
- x = self.schedule.pop(self.schedule.index(task))
+ self.schedule.pop(self.schedule.index(task))
else:
updated = True
self.schedule_lock.acquire()
diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py
index 721181a8..96b4f181 100644
--- a/sleekxmpp/xmlstream/stanzabase.py
+++ b/sleekxmpp/xmlstream/stanzabase.py
@@ -14,7 +14,6 @@
import copy
import logging
-import sys
import weakref
from xml.etree import cElementTree as ET
@@ -77,6 +76,49 @@ def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False):
registerStanzaPlugin = register_stanza_plugin
+def fix_ns(xpath, split=False, propagate_ns=True, default_ns=''):
+ """Apply the stanza's namespace to elements in an XPath expression.
+
+ :param string xpath: The XPath expression to fix with namespaces.
+ :param bool split: Indicates if the fixed XPath should be left as a
+ list of element names with namespaces. Defaults to
+ False, which returns a flat string path.
+ :param bool propagate_ns: Overrides propagating parent element
+ namespaces to child elements. Useful if
+ you wish to simply split an XPath that has
+ non-specified namespaces, and child and
+ parent namespaces are known not to always
+ match. Defaults to True.
+ """
+ fixed = []
+ # Split the XPath into a series of blocks, where a block
+ # is started by an element with a namespace.
+ ns_blocks = xpath.split('{')
+ for ns_block in ns_blocks:
+ if '}' in ns_block:
+ # Apply the found namespace to following elements
+ # that do not have namespaces.
+ namespace = ns_block.split('}')[0]
+ elements = ns_block.split('}')[1].split('/')
+ else:
+ # Apply the stanza's namespace to the following
+ # elements since no namespace was provided.
+ namespace = default_ns
+ elements = ns_block.split('/')
+
+ for element in elements:
+ if element:
+ # Skip empty entry artifacts from splitting.
+ if propagate_ns:
+ tag = '{%s}%s' % (namespace, element)
+ else:
+ tag = element
+ fixed.append(tag)
+ if split:
+ return fixed
+ return '/'.join(fixed)
+
+
class ElementBase(object):
"""
@@ -309,6 +351,7 @@ class ElementBase(object):
if self.xml is None:
self.xml = xml
+ last_xml = self.xml
if self.xml is None:
# Generate XML from the stanza definition
for ename in self.name.split('/'):
@@ -345,7 +388,8 @@ class ElementBase(object):
"""
if attrib not in self.plugins:
plugin_class = self.plugin_attrib_map[attrib]
- plugin = plugin_class(parent=self)
+ existing_xml = self.xml.find(plugin_class.tag_name())
+ plugin = plugin_class(parent=self, xml=existing_xml)
self.plugins[attrib] = plugin
if plugin_class in self.plugin_iterables:
self.iterables.append(plugin)
@@ -759,7 +803,7 @@ class ElementBase(object):
may be either a string or a list of element
names with attribute checks.
"""
- if isinstance(xpath, str):
+ if not isinstance(xpath, list):
xpath = self._fix_ns(xpath, split=True, propagate_ns=False)
# Extract the tag name and attribute checks for the first XPath node.
@@ -917,8 +961,9 @@ class ElementBase(object):
Any attribute values will be preserved.
"""
- for child in self.xml.getchildren():
+ for child in list(self.xml):
self.xml.remove(child)
+
for plugin in list(self.plugins.keys()):
del self.plugins[plugin]
return self
@@ -951,46 +996,9 @@ class ElementBase(object):
return self
def _fix_ns(self, xpath, split=False, propagate_ns=True):
- """Apply the stanza's namespace to elements in an XPath expression.
-
- :param string xpath: The XPath expression to fix with namespaces.
- :param bool split: Indicates if the fixed XPath should be left as a
- list of element names with namespaces. Defaults to
- False, which returns a flat string path.
- :param bool propagate_ns: Overrides propagating parent element
- namespaces to child elements. Useful if
- you wish to simply split an XPath that has
- non-specified namespaces, and child and
- parent namespaces are known not to always
- match. Defaults to True.
- """
- fixed = []
- # Split the XPath into a series of blocks, where a block
- # is started by an element with a namespace.
- ns_blocks = xpath.split('{')
- for ns_block in ns_blocks:
- if '}' in ns_block:
- # Apply the found namespace to following elements
- # that do not have namespaces.
- namespace = ns_block.split('}')[0]
- elements = ns_block.split('}')[1].split('/')
- else:
- # Apply the stanza's namespace to the following
- # elements since no namespace was provided.
- namespace = self.namespace
- elements = ns_block.split('/')
-
- for element in elements:
- if element:
- # Skip empty entry artifacts from splitting.
- if propagate_ns:
- tag = '{%s}%s' % (namespace, element)
- else:
- tag = element
- fixed.append(tag)
- if split:
- return fixed
- return '/'.join(fixed)
+ return fix_ns(xpath, split=split,
+ propagate_ns=propagate_ns,
+ default_ns=self.namespace)
def __eq__(self, other):
"""Compare the stanza object with another to test for equality.
@@ -1251,7 +1259,7 @@ class StanzaBase(ElementBase):
stanza sent immediately. Useful for stream
initialization. Defaults to ``False``.
"""
- self.stream.send_raw(self.__str__(), now=now)
+ self.stream.send(self, now=now)
def __copy__(self):
"""Return a copy of the stanza object that does not share the
diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py
index fb9f91bc..6ba82c37 100644
--- a/sleekxmpp/xmlstream/xmlstream.py
+++ b/sleekxmpp/xmlstream/xmlstream.py
@@ -24,7 +24,6 @@ import ssl
import sys
import threading
import time
-import types
import random
import weakref
try:
@@ -32,10 +31,12 @@ try:
except ImportError:
import Queue as queue
+from xml.parsers.expat import ExpatError
+
import sleekxmpp
from sleekxmpp.thirdparty.statemachine import StateMachine
from sleekxmpp.xmlstream import Scheduler, tostring
-from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
+from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET, ElementBase
from sleekxmpp.xmlstream.handler import Waiter, XMLCallback
from sleekxmpp.xmlstream.matcher import MatchXMLMask
@@ -80,6 +81,12 @@ SSL_RETRY_MAX = 10
#: Maximum time to delay between connection attempts is one hour.
RECONNECT_MAX_DELAY = 600
+#: Maximum number of attempts to connect to the server before quitting
+#: and raising a 'connect_failed' event. Setting this to ``None`` will
+#: allow infinite reconnection attempts, and using ``0`` will disable
+#: reconnections. Defaults to ``None``.
+RECONNECT_MAX_ATTEMPTS = None
+
log = logging.getLogger(__name__)
@@ -156,6 +163,12 @@ class XMLStream(object):
#: Maximum time to delay between connection attempts is one hour.
self.reconnect_max_delay = RECONNECT_MAX_DELAY
+ #: Maximum number of attempts to connect to the server before
+ #: quitting and raising a 'connect_failed' event. Setting to
+ #: ``None`` allows infinite reattempts, while setting it to ``0``
+ #: will disable reconnection attempts. Defaults to ``None``.
+ self.reconnect_max_attempts = RECONNECT_MAX_ATTEMPTS
+
#: The time in seconds to delay between attempts to resend data
#: after an SSL error.
self.ssl_retry_max = SSL_RETRY_MAX
@@ -254,6 +267,7 @@ class XMLStream(object):
#: A queue of string data to be sent over the stream.
self.send_queue = queue.Queue()
+ self.send_queue_lock = threading.Lock()
#: A :class:`~sleekxmpp.xmlstream.scheduler.Scheduler` instance for
#: executing callbacks in the future based on time delays.
@@ -268,6 +282,7 @@ class XMLStream(object):
self.__handlers = []
self.__event_handlers = {}
self.__event_handlers_lock = threading.Lock()
+ self.__filters = {'in': [], 'out': [], 'out_sync': []}
self._id = 0
self._id_lock = threading.Lock()
@@ -355,8 +370,10 @@ class XMLStream(object):
use_tls=True, reattempt=True):
"""Create a new socket and connect to the server.
- Setting ``reattempt`` to ``True`` will cause connection attempts to
- be made every second until a successful connection is established.
+ Setting ``reattempt`` to ``True`` will cause connection
+ attempts to be made with an exponential backoff delay (max of
+ :attr:`reconnect_max_delay` which defaults to 10 minute) until a
+ successful connection is established.
:param host: The name of the desired server for the connection.
:param port: Port to connect to on the server.
@@ -381,25 +398,31 @@ class XMLStream(object):
if use_tls is not None:
self.use_tls = use_tls
+
# Repeatedly attempt to connect until a successful connection
# is established.
+ attempts = self.reconnect_max_attempts
connected = self.state.transition('disconnected', 'connected',
- func=self._connect)
+ func=self._connect, args=(reattempt,))
while reattempt and not connected and not self.stop.is_set():
connected = self.state.transition('disconnected', 'connected',
func=self._connect)
+ if not connected:
+ if attempts is not None:
+ attempts -= 1
+ if attempts <= 0:
+ self.event('connection_failed', direct=True)
+ return False
return connected
- def _connect(self):
+ def _connect(self, reattempt=True):
self.scheduler.remove('Session timeout check')
self.stop.clear()
if self.default_domain:
self.address = self.pick_dns_answer(self.default_domain,
self.address[1])
- self.socket = self.socket_class(Socket.AF_INET, Socket.SOCK_STREAM)
- self.configure_socket()
-
- if self.reconnect_delay is None:
+
+ if self.reconnect_delay is None or not reattempt:
delay = 1.0
else:
delay = min(self.reconnect_delay * 2, self.reconnect_max_delay)
@@ -417,10 +440,33 @@ class XMLStream(object):
self.stop.set()
return False
+ try:
+ # Look for IPv6 addresses, in addition to IPv4
+ for res in Socket.getaddrinfo(self.address[0],
+ int(self.address[1]),
+ 0,
+ Socket.SOCK_STREAM):
+ log.debug("Trying: %s", res[-1])
+ af, sock_type, proto, canonical, sock_addr = res
+ try:
+ self.socket = self.socket_class(af, sock_type, proto)
+ break
+ except Socket.error:
+ log.debug("Could not open IPv%s socket." % proto)
+ except Socket.gaierror:
+ log.warning("Socket could not be opened: no connectivity" + \
+ " or wrong IP versions.")
+ if reattempt:
+ self.reconnect_delay = delay
+ return False
+
+ self.configure_socket()
+
if self.use_proxy:
connected = self._connect_proxy()
if not connected:
- self.reconnect_delay = delay
+ if reattempt:
+ self.reconnect_delay = delay
return False
if self.use_ssl and self.ssl_support:
@@ -446,6 +492,12 @@ class XMLStream(object):
log.debug("Connecting to %s:%s", *self.address)
self.socket.connect(self.address)
+ if self.use_ssl and self.ssl_support:
+ cert = self.socket.getpeercert(binary_form=True)
+ cert = ssl.DER_cert_to_PEM_cert(cert)
+ log.debug('CERT: %s', cert)
+ self.event('ssl_cert', cert, direct=True)
+
self.set_socket(self.socket, ignore=True)
#this event is where you should set your application state
self.event("connected", direct=True)
@@ -453,10 +505,11 @@ class XMLStream(object):
return True
except Socket.error as serr:
error_msg = "Could not connect to %s:%s. Socket Error #%s: %s"
- self.event('socket_error', serr)
+ self.event('socket_error', serr, direct=True)
log.error(error_msg, self.address[0], self.address[1],
serr.errno, serr.strerror)
- self.reconnect_delay = delay
+ if reattempt:
+ self.reconnect_delay = delay
return False
def _connect_proxy(self):
@@ -506,7 +559,7 @@ class XMLStream(object):
return True
except Socket.error as serr:
error_msg = "Could not connect to %s:%s. Socket Error #%s: %s"
- self.event('socket_error', serr)
+ self.event('socket_error', serr, direct=True)
log.error(error_msg, self.address[0], self.address[1],
serr.errno, serr.strerror)
return False
@@ -550,6 +603,7 @@ class XMLStream(object):
:attr:`disconnect_wait`.
"""
self.state.transition('connected', 'disconnected',
+ wait=2.0,
func=self._disconnect, args=(reconnect, wait))
def _disconnect(self, reconnect=False, wait=None):
@@ -577,7 +631,7 @@ class XMLStream(object):
self.socket.close()
self.filesocket.close()
except Socket.error as serr:
- self.event('socket_error', serr)
+ self.event('socket_error', serr, direct=True)
finally:
#clear your application state
self.event("disconnected", direct=True)
@@ -590,6 +644,8 @@ class XMLStream(object):
self.state.transition('connected', 'disconnected', wait=2.0,
func=self._disconnect, args=(True,))
+ attempts = self.reconnect_max_attempts
+
log.debug("connecting...")
connected = self.state.transition('disconnected', 'connected',
wait=2.0, func=self._connect)
@@ -597,6 +653,12 @@ class XMLStream(object):
connected = self.state.transition('disconnected', 'connected',
wait=2.0, func=self._connect)
connected = connected or self.state.ensure('connected')
+ if not connected:
+ if attempts is not None:
+ attempts -= 1
+ if attempts <= 0:
+ self.event('connection_failed', direct=True)
+ return False
return connected
def set_socket(self, socket, ignore=False):
@@ -674,6 +736,12 @@ class XMLStream(object):
else:
self.socket = ssl_socket
self.socket.do_handshake()
+
+ cert = self.socket.getpeercert(binary_form=True)
+ cert = ssl.DER_cert_to_PEM_cert(cert)
+ log.debug('CERT: %s', cert)
+ self.event('ssl_cert', cert, direct=True)
+
self.set_socket(self.socket)
return True
else:
@@ -741,7 +809,29 @@ class XMLStream(object):
stanza objects, but may still be processed using handlers and
matchers.
"""
- del self.__root_stanza[stanza_class]
+ self.__root_stanza.remove(stanza_class)
+
+ def add_filter(self, mode, handler, order=None):
+ """Add a filter for incoming or outgoing stanzas.
+
+ These filters are applied before incoming stanzas are
+ passed to any handlers, and before outgoing stanzas
+ are put in the send queue.
+
+ Each filter must accept a single stanza, and return
+ either a stanza or ``None``. If the filter returns
+ ``None``, then the stanza will be dropped from being
+ processed for events or from being sent.
+
+ :param mode: One of ``'in'`` or ``'out'``.
+ :param handler: The filter function.
+ :param int order: The position to insert the filter in
+ the list of active filters.
+ """
+ if order:
+ self.__filters[mode].insert(order, handler)
+ else:
+ self.__filters[mode].append(handler)
def add_handler(self, mask, pointer, name=None, disposable=False,
threaded=False, filter=False, instream=False):
@@ -808,20 +898,44 @@ class XMLStream(object):
resolver = dns.resolver.get_default_resolver()
self.configure_dns(resolver, domain=domain, port=port)
+ v4_answers = []
+ v6_answers = []
+ answers = []
+
try:
- answers = resolver.query(domain, dns.rdatatype.A)
+ log.debug("Querying A records for %s" % domain)
+ v4_answers = resolver.query(domain, dns.rdatatype.A)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
log.warning("No A records for %s", domain)
- return [((domain, port), 0, 0)]
+ v4_answers = [((domain, port), 0, 0)]
except dns.exception.Timeout:
log.warning("DNS resolution timed out " + \
"for A record of %s", domain)
- return [((domain, port), 0, 0)]
+ v4_answers = [((domain, port), 0, 0)]
+ else:
+ for ans in v4_answers:
+ log.debug("Found A record: %s", ans.address)
+ answers.append(((ans.address, port), 0, 0))
+
+ try:
+ log.debug("Querying AAAA records for %s" % domain)
+ v6_answers = resolver.query(domain, dns.rdatatype.AAAA)
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ log.warning("No AAAA records for %s", domain)
+ v6_answers = [((domain, port), 0, 0)]
+ except dns.exception.Timeout:
+ log.warning("DNS resolution timed out " + \
+ "for AAAA record of %s", domain)
+ v6_answers = [((domain, port), 0, 0)]
else:
- return [((ans.address, port), 0, 0) for ans in answers]
+ for ans in v6_answers:
+ log.debug("Found AAAA record: %s", ans.address)
+ answers.append(((ans.address, port), 0, 0))
+
+ return answers
else:
log.warning("dnspython is not installed -- " + \
- "relying on OS A record resolution")
+ "relying on OS A/AAAA record resolution")
self.configure_dns(None, domain=domain, port=port)
return [((domain, port), 0, 0)]
@@ -850,6 +964,7 @@ class XMLStream(object):
items = [x for x in addresses.keys()]
items.sort()
+ address = (domain, port)
picked = random.randint(0, intmax)
for item in items:
if picked <= item:
@@ -857,8 +972,8 @@ class XMLStream(object):
break
for idx, answer in enumerate(self.dns_answers):
if self.dns_answers[0] == address:
+ self.dns_answers.pop(idx)
break
- self.dns_answers.pop(idx)
log.debug("Trying to connect to %s:%s", *address)
return address
@@ -971,7 +1086,7 @@ class XMLStream(object):
"""
return xml
- def send(self, data, mask=None, timeout=None, now=False):
+ def send(self, data, mask=None, timeout=None, now=False, use_filters=True):
"""A wrapper for :meth:`send_raw()` for sending stanza objects.
May optionally block until an expected response is received.
@@ -989,18 +1104,40 @@ class XMLStream(object):
sending the stanza immediately. Useful mainly
for stream initialization stanzas.
Defaults to ``False``.
+ :param bool use_filters: Indicates if outgoing filters should be
+ applied to the given stanza data. Disabling
+ filters is useful when resending stanzas.
+ Defaults to ``True``.
"""
if timeout is None:
timeout = self.response_timeout
if hasattr(mask, 'xml'):
mask = mask.xml
- data = str(data)
+
+ if isinstance(data, ElementBase):
+ if use_filters:
+ for filter in self.__filters['out']:
+ data = filter(data)
+ if data is None:
+ return
+
if mask is not None:
log.warning("Use of send mask waiters is deprecated.")
wait_for = Waiter("SendWait_%s" % self.new_id(),
MatchXMLMask(mask))
self.register_handler(wait_for)
- self.send_raw(data, now)
+
+ if isinstance(data, ElementBase):
+ with self.send_queue_lock:
+ if use_filters:
+ for filter in self.__filters['out_sync']:
+ data = filter(data)
+ if data is None:
+ return
+ str_data = str(data)
+ self.send_raw(str_data, now)
+ else:
+ self.send_raw(data, now)
if mask is not None:
return wait_for.wait(timeout)
@@ -1061,7 +1198,7 @@ class XMLStream(object):
if count > 1:
log.debug('SENT: %d chunks', count)
except Socket.error as serr:
- self.event('socket_error', serr)
+ self.event('socket_error', serr, direct=True)
log.warning("Failed to send %s", data)
if reconnect is None:
reconnect = self.auto_reconnect
@@ -1157,12 +1294,11 @@ class XMLStream(object):
except SystemExit:
log.debug("SystemExit in _process")
shutdown = True
- except SyntaxError as e:
+ except (SyntaxError, ExpatError) as e:
log.error("Error reading from XML stream.")
- shutdown = True
self.exception(e)
except Socket.error as serr:
- self.event('socket_error', serr)
+ self.event('socket_error', serr, direct=True)
log.exception('Socket Error')
except Exception as e:
if not self.stop.is_set():
@@ -1246,8 +1382,6 @@ class XMLStream(object):
:param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
stanza to analyze.
"""
- log.debug("RECV: %s", tostring(xml, xmlns=self.default_ns,
- stream=self))
# Apply any preprocessing filters.
xml = self.incoming_filter(xml)
@@ -1255,6 +1389,14 @@ class XMLStream(object):
# stanza type applies, a generic StanzaBase stanza will be used.
stanza = self._build_stanza(xml)
+ for filter in self.__filters['in']:
+ if stanza is not None:
+ stanza = filter(stanza)
+ if stanza is None:
+ return
+
+ log.debug("RECV: %s", stanza)
+
# Match the stanza against registered handlers. Handlers marked
# to run "in stream" will be executed immediately; the rest will
# be queued.
@@ -1371,7 +1513,7 @@ class XMLStream(object):
"""Extract stanzas from the send queue and send them on the stream."""
try:
while not self.stop.is_set():
- while not self.stop.is_set and \
+ while not self.stop.is_set() and \
not self.session_started_event.is_set():
self.session_started_event.wait(timeout=1)
if self.__failed_send_stanza is not None:
@@ -1398,9 +1540,7 @@ class XMLStream(object):
log.debug('SSL error - max retries reached')
self.exception(serr)
log.warning("Failed to send %s", data)
- if reconnect is None:
- reconnect = self.auto_reconnect
- self.disconnect(reconnect)
+ self.disconnect(self.auto_reconnect)
log.warning('SSL write error - reattempting')
time.sleep(self.ssl_retry_delay)
tries += 1
@@ -1408,7 +1548,7 @@ class XMLStream(object):
log.debug('SENT: %d chunks', count)
self.send_queue.task_done()
except Socket.error as serr:
- self.event('socket_error', serr)
+ self.event('socket_error', serr, direct=True)
log.warning("Failed to send %s", data)
self.__failed_send_stanza = data
self.disconnect(self.auto_reconnect)
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
new file mode 100644
index 00000000..6220d7a5
--- /dev/null
+++ b/tests/test_plugins.py
@@ -0,0 +1,162 @@
+import unittest
+import logging
+
+from sleekxmpp.plugins.base import PluginManager, BasePlugin, register_plugin
+
+
+class A(BasePlugin):
+ name = 'a'
+
+
+class B(BasePlugin):
+ name = 'b'
+
+
+class C(BasePlugin):
+ name = 'c'
+ dependencies = set(['b', 'd'])
+
+
+class D(BasePlugin):
+ name = 'd'
+ dependencies = set(['c'])
+
+
+class E(BasePlugin):
+ name = 'e'
+ dependencies = set(['a', 'd'])
+
+class F(BasePlugin):
+ name = 'f'
+ dependencies = set(['a', 'b'])
+
+
+register_plugin(A)
+register_plugin(B)
+register_plugin(C)
+register_plugin(D)
+register_plugin(E)
+register_plugin(F)
+
+
+class TestPlugins(unittest.TestCase):
+
+
+ def test_enable(self):
+ """Enable a single plugin."""
+ p = PluginManager(None)
+
+ events = []
+
+ def init(self):
+ events.append('init')
+
+ A.plugin_init = init
+
+ p.enable('a')
+
+ self.assertEqual(len(p), 1, "Wrong number of enabled plugins.")
+ self.assertEqual(events, ['init'], "Plugin init method not called.")
+
+ def test_disable(self):
+ """Disable a single plugin."""
+ p = PluginManager(None)
+
+ events = []
+
+ def init(self):
+ events.append('init')
+
+ def end(self):
+ events.append('end')
+
+ A.plugin_init = init
+ A.plugin_end = end
+
+ p.enable('a')
+ p.disable('a')
+
+ self.assertEqual(len(p), 0, "Wrong number of enabled plugins.")
+ self.assertEqual(events, ['init', 'end'],
+ "Plugin lifecycle methods not called.")
+
+ def test_enable_dependencies(self):
+ """Enable a plugin with acyclic dependencies."""
+ p = PluginManager(None)
+
+ events = []
+
+ A.plugin_init = lambda s: events.append('init_a')
+ B.plugin_init = lambda s: events.append('init_b')
+
+ p.enable('f')
+
+ self.assertEqual(len(p), 3, "Wrong number of enabled plugins.")
+ self.assertTrue('init_a' in events, "Dependency A not enabled.")
+ self.assertTrue('init_b' in events, "Dependency B not enabled.")
+
+ def test_enable_cyclic_dependencies(self):
+ """Enable a plugin with cyclic dependencies."""
+
+ p = PluginManager(None)
+
+ events = []
+
+ B.plugin_init = lambda s: events.append('init_b')
+ C.plugin_init = lambda s: events.append('init_c')
+ D.plugin_init = lambda s: events.append('init_d')
+
+ p.enable('c')
+
+ self.assertEqual(len(p), 3, "Wrong number of enabled plugins.")
+ self.assertTrue('init_b' in events, "Dependency B not enabled.")
+ self.assertTrue('init_c' in events, "Dependency C not enabled.")
+ self.assertTrue('init_d' in events, "Dependency D not enabled.")
+
+ def test_disable_dependendents(self):
+ """Disable a plugin with dependents."""
+
+ p = PluginManager(None)
+
+ events = []
+
+ A.plugin_end = lambda s: events.append('end_a')
+ B.plugin_end = lambda s: events.append('end_b')
+ F.plugin_end = lambda s: events.append('end_f')
+
+ p.enable('f')
+ p.disable('a')
+
+ self.assertEqual(len(p), 1, "Wrong number of enabled plugins.")
+ self.assertTrue('end_f' in events, "Dependent F not disabled.")
+ self.assertTrue('end_a' in events, "Plugin A not disabled.")
+
+ def test_disable_cyclic_dependents(self):
+ """Disable a plugin with cyclic dependents."""
+
+ p = PluginManager(None)
+
+ events = []
+
+ B.plugin_end = lambda s: events.append('end_b')
+ C.plugin_end = lambda s: events.append('end_c')
+ D.plugin_end = lambda s: events.append('end_d')
+
+ p.enable('c')
+ p.disable('b')
+
+ self.assertEqual(len(p), 0, "Wrong number of enabled plugins.")
+ self.assertTrue('end_b' in events, "Plugin B not disabled.")
+ self.assertTrue('end_c' in events, "Dependent C not disabled.")
+ self.assertTrue('end_d' in events, "Dependent D not disabled.")
+
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestPlugins)
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(levelname)-8s %(message)s')
+
+ tests = unittest.TestSuite([suite])
+ unittest.TextTestRunner(verbosity=2).run(tests)
diff --git a/tests/test_stanza_xep_0009.py b/tests/test_stanza_xep_0009.py
index 36800335..fa1ed19b 100644
--- a/tests/test_stanza_xep_0009.py
+++ b/tests/test_stanza_xep_0009.py
@@ -1,3 +1,5 @@
+# -*- encoding:utf-8 -*-
+
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
@@ -6,7 +8,10 @@
See the file LICENSE for copying permission.
"""
+from __future__ import unicode_literals
+
import base64
+import sys
from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, \
MethodResponse
@@ -19,6 +24,9 @@ from sleekxmpp.xmlstream.tostring import tostring
import unittest
+if sys.version_info > (3, 0):
+ unicode = str
+
class TestJabberRPC(SleekTest):
@@ -114,6 +122,24 @@ class TestJabberRPC(SleekTest):
self.assertEqual(params, xml2py(expected_xml),
"XML to string conversion")
+ def testConvertUnicodeString(self):
+ params = ["おはよう"]
+ params_xml = py2xml(*params)
+ expected_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <string>おはよう</string>
+ </value>
+ </param>
+ </params>
+ """)
+ self.assertTrue(self.compare(expected_xml, params_xml),
+ "String to XML conversion\nExpected: %s\nGot: %s" % (
+ tostring(expected_xml), tostring(params_xml)))
+ self.assertEqual(params, xml2py(expected_xml),
+ "XML to string conversion")
+
def testConvertInteger(self):
params = [32767, -32768]
params_xml = py2xml(*params)
diff --git a/tests/test_stanza_xep_0047.py b/tests/test_stanza_xep_0047.py
new file mode 100644
index 00000000..6aa2314b
--- /dev/null
+++ b/tests/test_stanza_xep_0047.py
@@ -0,0 +1,90 @@
+from sleekxmpp.test import *
+from sleekxmpp.plugins.xep_0047 import Data
+
+
+class TestIBB(SleekTest):
+
+ def setUp(self):
+ register_stanza_plugin(Iq, Data)
+
+ def testInvalidBase64MidEqual(self):
+ """
+ Test detecting invalid base64 data with = inside the
+ character data instead of at the end.
+ """
+ iq = Iq(xml=ET.fromstring("""
+ <iq type="set" id="0" to="tester@localhost">
+ <data xmlns="http://jabber.org/protocol/ibb" seq="0">
+ ABC=DEFGH
+ </data>
+ </iq>
+ """))
+
+ errored = False
+
+ try:
+ data = iq['ibb_data']['data']
+ except XMPPError:
+ errored = True
+
+ self.assertTrue(errored, "ABC=DEFGH did not raise base64 error")
+
+ def testInvalidBase64PrefixEqual(self):
+ """
+ Test detecting invalid base64 data with = as a prefix
+ to the character data.
+ """
+ iq = Iq(xml=ET.fromstring("""
+ <iq type="set" id="0" to="tester@localhost">
+ <data xmlns="http://jabber.org/protocol/ibb" seq="0">
+ =ABCDEFGH
+ </data>
+ </iq>
+ """))
+
+ errored = False
+
+ try:
+ data = iq['ibb_data']['data']
+ except XMPPError:
+ errored = True
+
+ self.assertTrue(errored, "=ABCDEFGH did not raise base64 error")
+
+ def testInvalidBase64Alphabet(self):
+ """
+ Test detecting invalid base64 data with characters
+ outside of the base64 alphabet.
+ """
+ iq = Iq(xml=ET.fromstring("""
+ <iq type="set" id="0" to="tester@localhost">
+ <data xmlns="http://jabber.org/protocol/ibb" seq="0">
+ ABCD?EFGH
+ </data>
+ </iq>
+ """))
+
+ errored = False
+
+ try:
+ data = iq['ibb_data']['data']
+ except XMPPError:
+ errored = True
+
+ self.assertTrue(errored, "ABCD?EFGH did not raise base64 error")
+
+ def testConvertData(self):
+ """Test that data is converted to base64"""
+ iq = Iq()
+ iq['type'] = 'set'
+ iq['ibb_data']['seq'] = 0
+ iq['ibb_data']['data'] = 'sleekxmpp'
+
+ self.check(iq, """
+ <iq type="set">
+ <data xmlns="http://jabber.org/protocol/ibb" seq="0">c2xlZWt4bXBw</data>
+ </iq>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestIBB)
diff --git a/tests/test_stanza_xep_0085.py b/tests/test_stanza_xep_0085.py
index b08404e2..61784e47 100644
--- a/tests/test_stanza_xep_0085.py
+++ b/tests/test_stanza_xep_0085.py
@@ -4,7 +4,11 @@ import sleekxmpp.plugins.xep_0085 as xep_0085
class TestChatStates(SleekTest):
def setUp(self):
- register_stanza_plugin(Message, xep_0085.ChatState)
+ register_stanza_plugin(Message, xep_0085.stanza.Active)
+ register_stanza_plugin(Message, xep_0085.stanza.Composing)
+ register_stanza_plugin(Message, xep_0085.stanza.Gone)
+ register_stanza_plugin(Message, xep_0085.stanza.Inactive)
+ register_stanza_plugin(Message, xep_0085.stanza.Paused)
def testCreateChatState(self):
"""Testing creating chat states."""
diff --git a/tests/test_stanza_xep_0184.py b/tests/test_stanza_xep_0184.py
new file mode 100644
index 00000000..13472373
--- /dev/null
+++ b/tests/test_stanza_xep_0184.py
@@ -0,0 +1,38 @@
+from sleekxmpp.test import *
+import sleekxmpp.plugins.xep_0184 as xep_0184
+
+
+class TestReciept(SleekTest):
+
+ def setUp(self):
+ register_stanza_plugin(Message, xep_0184.Request)
+ register_stanza_plugin(Message, xep_0184.Received)
+
+ def testCreateRequest(self):
+ request = """
+ <message>
+ <request xmlns="urn:xmpp:receipts" />
+ </message>
+ """
+
+ msg = self.Message()
+
+ self.assertEqual(msg['request_receipt'], False)
+
+ msg['request_receipt'] = True
+ self.check(msg, request)
+
+ def testCreateReceived(self):
+ received = """
+ <message>
+ <received xmlns="urn:xmpp:receipts" id="1" />
+ </message>
+ """
+
+ msg = self.Message()
+
+ msg['receipt'] = '1'
+ self.check(msg, received)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestReciept)
diff --git a/tests/test_stream_filters.py b/tests/test_stream_filters.py
new file mode 100644
index 00000000..ef4d5dc8
--- /dev/null
+++ b/tests/test_stream_filters.py
@@ -0,0 +1,88 @@
+import time
+
+from sleekxmpp import Message
+from sleekxmpp.test import *
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.xmlstream.matcher import *
+
+
+class TestFilters(SleekTest):
+
+ """
+ Test using incoming and outgoing filters.
+ """
+
+ def setUp(self):
+ self.stream_start()
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testIncoming(self):
+
+ data = []
+
+ def in_filter(stanza):
+ if isinstance(stanza, Message):
+ if stanza['body'] == 'testing':
+ stanza['subject'] = stanza['body'] + ' filter'
+ print('>>> %s' % stanza['subject'])
+ return stanza
+
+ def on_message(msg):
+ print('<<< %s' % msg['subject'])
+ data.append(msg['subject'])
+
+ self.xmpp.add_filter('in', in_filter)
+ self.xmpp.add_event_handler('message', on_message)
+
+ self.recv("""
+ <message>
+ <body>no filter</body>
+ </message>
+ """)
+
+ self.recv("""
+ <message>
+ <body>testing</body>
+ </message>
+ """)
+
+ time.sleep(0.5)
+
+ self.assertEqual(data, ['', 'testing filter'],
+ 'Incoming filter did not apply %s' % data)
+
+ def testOutgoing(self):
+
+ def out_filter(stanza):
+ if isinstance(stanza, Message):
+ if stanza['body'] == 'testing':
+ stanza['body'] = 'changed!'
+ return stanza
+
+ self.xmpp.add_filter('out', out_filter)
+
+ m1 = self.Message()
+ m1['body'] = 'testing'
+ m1.send()
+
+ m2 = self.Message()
+ m2['body'] = 'blah'
+ m2.send()
+
+ self.send("""
+ <message>
+ <body>changed!</body>
+ </message>
+ """)
+
+ self.send("""
+ <message>
+ <body>blah</body>
+ </message>
+ """)
+
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestFilters)
diff --git a/tests/test_stream_presence.py b/tests/test_stream_presence.py
index 63ccb043..4f2ede16 100644
--- a/tests/test_stream_presence.py
+++ b/tests/test_stream_presence.py
@@ -7,6 +7,9 @@ class TestStreamPresence(SleekTest):
Test handling roster updates.
"""
+ def setUp(self):
+ self.stream_start(jid='tester@localhost', plugins=[])
+
def tearDown(self):
self.stream_close()
@@ -25,7 +28,6 @@ class TestStreamPresence(SleekTest):
# The presence_unavailable event should be triggered.
events.add('unavailable')
- self.stream_start()
self.xmpp.add_event_handler('got_offline', got_offline)
self.xmpp.add_event_handler('presence_unavailable', unavailable)
@@ -48,7 +50,6 @@ class TestStreamPresence(SleekTest):
def got_offline(presence):
events.append('got_offline')
- self.stream_start()
self.xmpp.add_event_handler('got_offline', got_offline)
# Setup roster. Use a 'set' instead of 'result' so we
@@ -98,7 +99,6 @@ class TestStreamPresence(SleekTest):
def got_online(p):
events.add('got_online')
- self.stream_start()
self.xmpp.add_event_handler('presence_available', presence_available)
self.xmpp.add_event_handler('got_online', got_online)
@@ -128,7 +128,6 @@ class TestStreamPresence(SleekTest):
def changed_subscription(p):
events.add('changed_subscription')
- self.stream_start(jid='tester@localhost')
self.xmpp.add_event_handler('changed_subscription',
changed_subscription)
@@ -175,8 +174,6 @@ class TestStreamPresence(SleekTest):
def changed_subscription(p):
events.add('changed_subscription')
- self.stream_start(jid='tester@localhost')
-
self.xmpp.add_event_handler('changed_subscription',
changed_subscription)
self.xmpp.add_event_handler('presence_subscribe',
@@ -205,8 +202,6 @@ class TestStreamPresence(SleekTest):
events = []
- self.stream_start()
-
ptypes = ['available', 'away', 'dnd', 'xa', 'chat',
'unavailable', 'subscribe', 'subscribed',
'unsubscribe', 'unsubscribed']
@@ -254,7 +249,6 @@ class TestStreamPresence(SleekTest):
def test_changed_status(self):
"""Test that the changed_status event is handled properly."""
events = []
- self.stream_start()
def changed_status(presence):
events.append(presence['type'])
diff --git a/tests/test_stream_roster.py b/tests/test_stream_roster.py
index 1f83f0ec..2d0da173 100644
--- a/tests/test_stream_roster.py
+++ b/tests/test_stream_roster.py
@@ -104,6 +104,74 @@ class TestStreamRoster(SleekTest):
self.failUnless('roster_update' in events,
"Roster updated event not triggered: %s" % events)
+ def testRosterPushRemove(self):
+ """Test handling roster item removal updates."""
+ self.stream_start(mode='client')
+ events = []
+
+ # Add roster item
+ self.recv("""
+ <iq to='tester@localhost' type="set" id="1">
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@localhost"
+ name="User"
+ subscription="both">
+ <group>Friends</group>
+ <group>Examples</group>
+ </item>
+ </query>
+ </iq>
+ """)
+ self.send("""
+ <iq type="result" id="1">
+ <query xmlns="jabber:iq:roster" />
+ </iq>
+ """)
+
+ self.assertTrue('user@localhost' in self.xmpp.client_roster)
+
+ # Receive item remove push
+ self.recv("""
+ <iq to='tester@localhost' type="set" id="1">
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@localhost"
+ subscription="remove">
+ </item>
+ </query>
+ </iq>
+ """)
+ self.send("""
+ <iq type="result" id="1">
+ <query xmlns="jabber:iq:roster" />
+ </iq>
+ """)
+
+ self.assertTrue('user@localhost' not in self.xmpp.client_roster)
+
+ def testUnauthorizedRosterPush(self):
+ """Test rejecting a roster push from an unauthorized source."""
+ self.stream_start()
+ self.recv("""
+ <iq to='tester@localhost' from="malicious_user@localhost"
+ type="set" id="1">
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@localhost"
+ name="User"
+ subscription="both">
+ <group>Friends</group>
+ <group>Examples</group>
+ </item>
+ </query>
+ </iq>
+ """)
+ self.send("""
+ <iq to="malicious_user@localhost" type="error" id="1">
+ <error type="cancel" code="503">
+ <service-unavailable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </iq>
+ """)
+
def testRosterTimeout(self):
"""Test handling a timed out roster request."""
self.stream_start()
@@ -125,7 +193,8 @@ class TestStreamRoster(SleekTest):
# Since get_roster blocks, we need to run it in a thread.
t = threading.Thread(name='get_roster',
target=self.xmpp.get_roster,
- kwargs={str('callback'): roster_callback})
+ kwargs={str('block'): False,
+ str('callback'): roster_callback})
t.start()
self.send("""
@@ -157,7 +226,7 @@ class TestStreamRoster(SleekTest):
def testRosterUnicode(self):
"""Test that JIDs with Unicode values are handled properly."""
- self.stream_start()
+ self.stream_start(plugins=[])
self.recv("""
<iq to="tester@localhost" type="set" id="1">
<query xmlns="jabber:iq:roster">
@@ -198,7 +267,7 @@ class TestStreamRoster(SleekTest):
def testSendLastPresence(self):
"""Test that sending the last presence works."""
- self.stream_start()
+ self.stream_start(plugins=[])
self.xmpp.send_presence(pshow='dnd')
self.xmpp.auto_authorize = True
self.xmpp.auto_subscribe = True
@@ -226,5 +295,62 @@ class TestStreamRoster(SleekTest):
</presence>
""")
+ def testUnsupportedRosterVer(self):
+ """Test working with a server without roster versioning."""
+ self.stream_start()
+ self.assertTrue('rosterver' not in self.xmpp.features)
+
+ t = threading.Thread(name='get_roster', target=self.xmpp.get_roster)
+ t.start()
+ self.send("""
+ <iq type="get" id="1">
+ <query xmlns="jabber:iq:roster" />
+ </iq>
+ """)
+ self.recv("""
+ <iq to="tester@localhost" type="result" id="1" />
+ """)
+
+ t.join()
+
+ def testBootstrapRosterVer(self):
+ """Test bootstrapping with roster versioning."""
+ self.stream_start()
+ self.xmpp.features.add('rosterver')
+ self.xmpp.client_roster.version = ''
+
+ t = threading.Thread(name='get_roster', target=self.xmpp.get_roster)
+ t.start()
+ self.send("""
+ <iq type="get" id="1">
+ <query xmlns="jabber:iq:roster" ver="" />
+ </iq>
+ """)
+ self.recv("""
+ <iq to="tester@localhost" type="result" id="1" />
+ """)
+
+ t.join()
+
+
+ def testExistingRosterVer(self):
+ """Test using a stored roster version."""
+ self.stream_start()
+ self.xmpp.features.add('rosterver')
+ self.xmpp.client_roster.version = '42'
+
+ t = threading.Thread(name='get_roster', target=self.xmpp.get_roster)
+ t.start()
+ self.send("""
+ <iq type="get" id="1">
+ <query xmlns="jabber:iq:roster" ver="42" />
+ </iq>
+ """)
+ self.recv("""
+ <iq to="tester@localhost" type="result" id="1" />
+ """)
+
+ t.join()
+
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamRoster)
diff --git a/tests/test_stream_xep_0030.py b/tests/test_stream_xep_0030.py
index 1666d3a1..dd43778a 100644
--- a/tests/test_stream_xep_0030.py
+++ b/tests/test_stream_xep_0030.py
@@ -122,7 +122,7 @@ class TestStreamDisco(SleekTest):
self.stream_start(mode='client',
plugins=['xep_0030'])
- def dynamic_jid(jid, node, iq):
+ def dynamic_jid(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node
result.add_identity('client', 'console', name='Dynamic Info')
@@ -158,7 +158,7 @@ class TestStreamDisco(SleekTest):
jid='tester.localhost',
plugins=['xep_0030'])
- def dynamic_global(jid, node, iq):
+ def dynamic_global(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node
result.add_identity('component', 'generic', name='Dynamic Info')
@@ -194,7 +194,7 @@ class TestStreamDisco(SleekTest):
self.stream_start(mode='client',
plugins=['xep_0030'])
- def dynamic_jid(jid, node, iq):
+ def dynamic_jid(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node
result.add_identity('client', 'console', name='Dynamic Info')
@@ -236,7 +236,7 @@ class TestStreamDisco(SleekTest):
jid='tester.localhost',
plugins=['xep_0030'])
- def dynamic_global(jid, node, iq):
+ def dynamic_global(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node
result.add_identity('component', 'generic', name='Dynamic Info')
@@ -325,7 +325,7 @@ class TestStreamDisco(SleekTest):
self.stream_start(mode='client',
plugins=['xep_0030'])
- def dynamic_jid(jid, node, iq):
+ def dynamic_jid(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node
result.add_item('tester@localhost', node='foo', name='JID')
@@ -359,7 +359,7 @@ class TestStreamDisco(SleekTest):
jid='tester.localhost',
plugins=['xep_0030'])
- def dynamic_global(jid, node, iq):
+ def dynamic_global(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node
result.add_item('tester@localhost', node='foo', name='Global')
@@ -393,7 +393,7 @@ class TestStreamDisco(SleekTest):
self.stream_start(mode='client',
plugins=['xep_0030'])
- def dynamic_jid(jid, node, iq):
+ def dynamic_jid(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node
result.add_item('tester@localhost', node='foo', name='Global')
@@ -435,7 +435,7 @@ class TestStreamDisco(SleekTest):
jid='tester.localhost',
plugins=['xep_0030'])
- def dynamic_global(jid, node, iq):
+ def dynamic_global(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node
result.add_item('tester.localhost', node='foo', name='Global')
diff --git a/tests/test_stream_xep_0047.py b/tests/test_stream_xep_0047.py
new file mode 100644
index 00000000..d8cdd6a3
--- /dev/null
+++ b/tests/test_stream_xep_0047.py
@@ -0,0 +1,180 @@
+import threading
+import time
+
+from sleekxmpp.test import *
+
+
+class TestInBandByteStreams(SleekTest):
+
+ def setUp(self):
+ self.stream_start(plugins=['xep_0047', 'xep_0030'])
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testOpenStream(self):
+ """Test requesting a stream, successfully"""
+
+ events = []
+
+ def on_stream_start(stream):
+ events.append('ibb_stream_start')
+
+
+ self.xmpp.add_event_handler('ibb_stream_start', on_stream_start)
+
+ t = threading.Thread(name='open_stream',
+ target=self.xmpp['xep_0047'].open_stream,
+ args=('tester@localhost/receiver',),
+ kwargs={'sid': 'testing'})
+ t.start()
+
+ self.send("""
+ <iq type="set" to="tester@localhost/receiver" id="1">
+ <open xmlns="http://jabber.org/protocol/ibb"
+ sid="testing"
+ block-size="4096"
+ stanza="iq" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq type="result" id="1"
+ to="tester@localhost"
+ from="tester@localhost/receiver" />
+ """)
+
+ t.join()
+
+ time.sleep(0.2)
+
+ self.assertEqual(events, ['ibb_stream_start'])
+
+ def testAysncOpenStream(self):
+ """Test requesting a stream, aysnc"""
+
+ events = set()
+
+ def on_stream_start(stream):
+ events.add('ibb_stream_start')
+
+ def stream_callback(iq):
+ events.add('callback')
+
+ self.xmpp.add_event_handler('ibb_stream_start', on_stream_start)
+
+ t = threading.Thread(name='open_stream',
+ target=self.xmpp['xep_0047'].open_stream,
+ args=('tester@localhost/receiver',),
+ kwargs={'sid': 'testing',
+ 'block': False,
+ 'callback': stream_callback})
+ t.start()
+
+ self.send("""
+ <iq type="set" to="tester@localhost/receiver" id="1">
+ <open xmlns="http://jabber.org/protocol/ibb"
+ sid="testing"
+ block-size="4096"
+ stanza="iq" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq type="result" id="1"
+ to="tester@localhost"
+ from="tester@localhost/receiver" />
+ """)
+
+ t.join()
+
+ time.sleep(0.2)
+
+ self.assertEqual(events, set(['ibb_stream_start', 'callback']))
+
+ def testSendData(self):
+ """Test sending data over an in-band bytestream."""
+
+ streams = []
+ data = []
+
+ def on_stream_start(stream):
+ streams.append(stream)
+
+ def on_stream_data(d):
+ data.append(d['data'])
+
+ self.xmpp.add_event_handler('ibb_stream_start', on_stream_start)
+ self.xmpp.add_event_handler('ibb_stream_data', on_stream_data)
+
+ t = threading.Thread(name='open_stream',
+ target=self.xmpp['xep_0047'].open_stream,
+ args=('tester@localhost/receiver',),
+ kwargs={'sid': 'testing'})
+ t.start()
+
+ self.send("""
+ <iq type="set" to="tester@localhost/receiver" id="1">
+ <open xmlns="http://jabber.org/protocol/ibb"
+ sid="testing"
+ block-size="4096"
+ stanza="iq" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq type="result" id="1"
+ to="tester@localhost"
+ from="tester@localhost/receiver" />
+ """)
+
+ t.join()
+
+ time.sleep(0.2)
+
+ stream = streams[0]
+
+
+ # Test sending data out
+ stream.send("Testing")
+
+ self.send("""
+ <iq type="set" id="2"
+ from="tester@localhost"
+ to="tester@localhost/receiver">
+ <data xmlns="http://jabber.org/protocol/ibb"
+ seq="0"
+ sid="testing">
+ VGVzdGluZw==
+ </data>
+ </iq>
+ """)
+
+ self.recv("""
+ <iq type="result" id="2"
+ to="tester@localhost"
+ from="tester@localhost/receiver" />
+ """)
+
+ # Test receiving data
+ self.recv("""
+ <iq type="set" id="A"
+ to="tester@localhost"
+ from="tester@localhost/receiver">
+ <data xmlns="http://jabber.org/protocol/ibb"
+ seq="0"
+ sid="testing">
+ aXQgd29ya3Mh
+ </data>
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="A"
+ to="tester@localhost/receiver" />
+ """)
+
+ self.assertEqual(data, ['it works!'])
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestInBandByteStreams)