Compare commits
238 Commits
exp_idle_c
...
slix-1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b549db959a | ||
|
|
d5188ac68a | ||
|
|
ada9444bf8 | ||
|
|
acc52fd935 | ||
|
|
1100ff1feb | ||
|
|
c17fc3a869 | ||
|
|
4dba697075 | ||
|
|
e42d651d7e | ||
|
|
4305eddb4f | ||
|
|
c2dc44cfd1 | ||
|
|
5fc14de32e | ||
|
|
d245558fd5 | ||
|
|
9d45370e8a | ||
|
|
cc1cc61d36 | ||
|
|
c6740a4908 | ||
|
|
55114bcffe | ||
|
|
4fa5dedc47 | ||
|
|
5525ef2285 | ||
|
|
a7ac969215 | ||
|
|
329cb5a9f8 | ||
|
|
d9b47b33f5 | ||
|
|
3582ac9941 | ||
|
|
2a127a57a7 | ||
|
|
7059400020 | ||
|
|
0b14ef82d4 | ||
|
|
83953af53d | ||
|
|
110cf25c6d | ||
|
|
f2bf6072ec | ||
|
|
5f9abe2e0e | ||
|
|
ea65b672e7 | ||
|
|
93c705fb31 | ||
|
|
0724f623bb | ||
|
|
82e549c0e9 | ||
|
|
1aa15792b4 | ||
|
|
ffb2b6bc04 | ||
|
|
27f98bf22c | ||
|
|
3978078710 | ||
|
|
00a0698720 | ||
|
|
4a24f58be2 | ||
|
|
da14ce16ec | ||
|
|
18e5abb9dd | ||
|
|
1a75b76916 | ||
|
|
53b56899a0 | ||
|
|
804b23d390 | ||
|
|
04eaf52b1d | ||
|
|
dc7fef1064 | ||
|
|
488c433555 | ||
|
|
9c5dd024b1 | ||
|
|
6e61adf3db | ||
|
|
041bd63864 | ||
|
|
a366482551 | ||
|
|
a721084f6e | ||
|
|
1b4187fa56 | ||
|
|
cf7a60705e | ||
|
|
349b05b9b7 | ||
|
|
9fbacf377a | ||
|
|
2da9e35cbc | ||
|
|
8adc8fa2ba | ||
|
|
9efa909dfc | ||
|
|
7f21fdbe26 | ||
|
|
f9c7fa92ea | ||
|
|
e75a160d52 | ||
|
|
170bd51387 | ||
|
|
abcec1e2d3 | ||
|
|
eeab646bfa | ||
|
|
2c69144189 | ||
|
|
f54ebec654 | ||
|
|
2ce931cb7a | ||
|
|
84eddd2ed2 | ||
|
|
2042e1a4d5 | ||
|
|
be14f0cc52 | ||
|
|
edd9199be8 | ||
|
|
bb094cc649 | ||
|
|
dbaa6ed952 | ||
|
|
8c94d894ab | ||
|
|
ffc7eac4dc | ||
|
|
555fd6d926 | ||
|
|
c024ac8f0b | ||
|
|
f00177c0cf | ||
|
|
d0ad25745a | ||
|
|
55be23a6da | ||
|
|
75ba283572 | ||
|
|
f7164d35d2 | ||
|
|
4afbb0322b | ||
|
|
7bce1ecc8a | ||
|
|
bbce16d526 | ||
|
|
c29fc39ef1 | ||
|
|
8335c08782 | ||
|
|
224d7ae133 | ||
|
|
04bff00171 | ||
|
|
f3e31baf04 | ||
|
|
9b25a7cf77 | ||
|
|
7a908ac07b | ||
|
|
92901637ec | ||
|
|
3590b663ed | ||
|
|
a33bde9cc3 | ||
|
|
ac50fdccfc | ||
|
|
a0c6bf15e9 | ||
|
|
a2852eb249 | ||
|
|
f1e6d6b0a9 | ||
|
|
116a33ba51 | ||
|
|
a8ac115310 | ||
|
|
1345b7c1d0 | ||
|
|
d60a652259 | ||
|
|
61a7cecb31 | ||
|
|
192b7e0349 | ||
|
|
80b60fc048 | ||
|
|
b8d7b9520c | ||
|
|
0305ce66b7 | ||
|
|
474405ab90 | ||
|
|
4415d3be1a | ||
|
|
058c530787 | ||
|
|
766d0dfd40 | ||
|
|
ac31913a65 | ||
|
|
d34ddf33db | ||
|
|
eb4e09b0ca | ||
|
|
ce085bf4f4 | ||
|
|
990113f8e7 | ||
|
|
aa022204ee | ||
|
|
c1f23b566b | ||
|
|
45f7cb8bda | ||
|
|
bdb1f66ac9 | ||
|
|
842157a6cc | ||
|
|
a63cc01482 | ||
|
|
1bbb6f3ff9 | ||
|
|
93894247a4 | ||
|
|
16bb5e2537 | ||
|
|
d19a6e05b2 | ||
|
|
86e85f9835 | ||
|
|
cc145d20b0 | ||
|
|
881d9040c4 | ||
|
|
1e77ea0944 | ||
|
|
140f0885b2 | ||
|
|
83f71a6610 | ||
|
|
271343a32d | ||
|
|
48857b0030 | ||
|
|
1fe7f5f4e6 | ||
|
|
d5b1904ebb | ||
|
|
b6b0e82dec | ||
|
|
632b7b4afe | ||
|
|
81b7b2c190 | ||
|
|
460de7d301 | ||
|
|
69022c6db7 | ||
|
|
0ef3fa2703 | ||
|
|
8da269de88 | ||
|
|
93ce318259 | ||
|
|
997928de91 | ||
|
|
83d00a5913 | ||
|
|
bf5d7c83af | ||
|
|
c66a4d4097 | ||
|
|
e112e86475 | ||
|
|
e034b31d6b | ||
|
|
18a4978456 | ||
|
|
17464b10a4 | ||
|
|
6fb3ecd414 | ||
|
|
c214e4f037 | ||
|
|
2ee05d9616 | ||
|
|
f795ac02e3 | ||
|
|
6e8235544c | ||
|
|
6e35948276 | ||
|
|
4da870fd19 | ||
|
|
cd7ff685fb | ||
|
|
1e4944d47e | ||
|
|
e68135f59f | ||
|
|
6408c5a747 | ||
|
|
115fe954ac | ||
|
|
3d243f7da5 | ||
|
|
ea5615f236 | ||
|
|
69da1c1d7c | ||
|
|
e85fa4203e | ||
|
|
506ca69917 | ||
|
|
8ac0ecdf40 | ||
|
|
dbd8115557 | ||
|
|
74b4ea20bf | ||
|
|
11fbaa4241 | ||
|
|
8fd0d7c993 | ||
|
|
1450d36377 | ||
|
|
06358d0665 | ||
|
|
2b3b86e281 | ||
|
|
92e4bc752a | ||
|
|
ffb2e05f21 | ||
|
|
1e2665df19 | ||
|
|
4d063e287e | ||
|
|
44f02fb3ab | ||
|
|
f6b3a0c6cf | ||
|
|
8b36e918e8 | ||
|
|
9044807121 | ||
|
|
24264d3a07 | ||
|
|
8bc70264ef | ||
|
|
957c635fb7 | ||
|
|
4027927c6e | ||
|
|
c16b862200 | ||
|
|
a96f608469 | ||
|
|
e1f25604ec | ||
|
|
0fe057b5c3 | ||
|
|
be76dda21d | ||
|
|
ecd124dd06 | ||
|
|
4a8951c4ee | ||
|
|
8afba7de85 | ||
|
|
1ce42d3a2f | ||
|
|
2f4d811db4 | ||
|
|
61127f521d | ||
|
|
62eefdbd6a | ||
|
|
225e07eb64 | ||
|
|
063e73c0d2 | ||
|
|
d261318e1a | ||
|
|
d33cc00fe9 | ||
|
|
27582f6fd2 | ||
|
|
e328ff4833 | ||
|
|
403462fdb8 | ||
|
|
f22d8e67b4 | ||
|
|
35f33f1614 | ||
|
|
c9f8ddff65 | ||
|
|
f5ae98aaf1 | ||
|
|
073e85381a | ||
|
|
afc939708f | ||
|
|
aabec8b993 | ||
|
|
e5e2fbb16b | ||
|
|
3dd379cdf1 | ||
|
|
a20582aba4 | ||
|
|
09cdbf1b76 | ||
|
|
ca306e7cec | ||
|
|
1bf34f7fe6 | ||
|
|
4144d60017 | ||
|
|
7265682a4d | ||
|
|
08c62a6bf1 | ||
|
|
d61f1cd035 | ||
|
|
1063feb33b | ||
|
|
79f3c1ac8f | ||
|
|
a5c03b763a | ||
|
|
3670d82f1c | ||
|
|
e94a73553d | ||
|
|
577fd71472 | ||
|
|
ef1c4368d0 | ||
|
|
48def71d0c | ||
|
|
c8c20fff71 | ||
|
|
75a18b5ffe | ||
|
|
ea3d39b50e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ slixmpp.egg-info/
|
||||
*~
|
||||
.baboon/
|
||||
.DS_STORE
|
||||
.idea/
|
||||
|
||||
10
.travis.yml
Normal file
10
.travis.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
install:
|
||||
- "pip install ."
|
||||
script: testall.py
|
||||
5
INSTALL
5
INSTALL
@@ -1,5 +1,6 @@
|
||||
Pre-requisites:
|
||||
- Python 3.1 or 2.6
|
||||
- Python 3.4
|
||||
- Cython 0.22 and libidn, optionally (making JID faster by compiling the stringprep module)
|
||||
|
||||
Install:
|
||||
> python3 setup.py install
|
||||
@@ -9,4 +10,4 @@ Root install:
|
||||
|
||||
To test:
|
||||
> cd examples
|
||||
> python echo_client.py -v -j [USER@example.com] -p [PASSWORD]
|
||||
> python3 echo_client.py -d -j [USER@example.com] -p [PASSWORD]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
include README.rst
|
||||
include LICENSE
|
||||
include testall.py
|
||||
include run_tests.py
|
||||
include slixmpp/stringprep.pyx
|
||||
recursive-include docs Makefile *.bat *.py *.rst *.css *.ttf *.png
|
||||
recursive-include examples *.py
|
||||
recursive-include tests *.py
|
||||
|
||||
16
README.rst
16
README.rst
@@ -8,6 +8,14 @@ Slixmpp's goals is to only rewrite the core of the library (the low level
|
||||
socket handling, the timers, the events dispatching) in order to remove all
|
||||
threads.
|
||||
|
||||
Building
|
||||
--------
|
||||
|
||||
Slixmpp can make use of cython to improve performance on critical modules.
|
||||
To do that, cython3 is necessary along with libidn headers. Otherwise,
|
||||
no compilation is needed. Building is done by running setup.py::
|
||||
|
||||
python3 setup.py build_ext --inplace
|
||||
|
||||
Documentation and Testing
|
||||
-------------------------
|
||||
@@ -21,7 +29,7 @@ be in ``docs/_build/html``::
|
||||
|
||||
To run the test suite for Slixmpp::
|
||||
|
||||
python testall.py
|
||||
python run_tests.py
|
||||
|
||||
|
||||
The Slixmpp Boilerplate
|
||||
@@ -88,7 +96,7 @@ Slixmpp projects::
|
||||
|
||||
xmpp = EchoBot('somejid@example.com', 'use_getpass')
|
||||
xmpp.connect()
|
||||
xmpp.process(block=True)
|
||||
xmpp.process(forever=True)
|
||||
|
||||
|
||||
Slixmpp Credits
|
||||
@@ -97,8 +105,8 @@ Slixmpp Credits
|
||||
**Maintainer of the slixmpp fork:** Florent Le Coz
|
||||
`louiz@louiz.org <xmpp:louiz@louiz.org?message>`_,
|
||||
|
||||
Credits
|
||||
-------
|
||||
Credits (SleekXMPP)
|
||||
-------------------
|
||||
|
||||
**Main Author:** Nathan Fritz
|
||||
`fritzy@netflint.net <xmpp:fritzy@netflint.net?message>`_,
|
||||
|
||||
8
docs/api/stanza/iq.rst
Normal file
8
docs/api/stanza/iq.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
IQ Stanza
|
||||
=========
|
||||
|
||||
.. module:: slixmpp.stanza
|
||||
|
||||
.. autoclass:: Iq
|
||||
:members:
|
||||
|
||||
7
docs/api/stanza/message.rst
Normal file
7
docs/api/stanza/message.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
Message Stanza
|
||||
==============
|
||||
|
||||
.. module:: slixmpp.stanza
|
||||
|
||||
.. autoclass:: Message
|
||||
:members:
|
||||
8
docs/api/stanza/presence.rst
Normal file
8
docs/api/stanza/presence.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
Presence Stanza
|
||||
===============
|
||||
|
||||
.. module:: slixmpp.stanza
|
||||
|
||||
.. autoclass:: Presence
|
||||
:members:
|
||||
|
||||
8
docs/api/stanza/rootstanza.rst
Normal file
8
docs/api/stanza/rootstanza.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
Root Stanza
|
||||
===========
|
||||
|
||||
.. module:: slixmpp.stanza.rootstanza
|
||||
|
||||
.. autoclass:: RootStanza
|
||||
:members:
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
.. module:: slixmpp.xmlstream.filesocket
|
||||
|
||||
.. _filesocket:
|
||||
|
||||
Python 2.6 File Socket Shims
|
||||
============================
|
||||
|
||||
.. autoclass:: FileSocket
|
||||
:members:
|
||||
|
||||
.. autoclass:: Socket26
|
||||
:members:
|
||||
@@ -10,15 +10,19 @@ The Basic Handler
|
||||
|
||||
Callback
|
||||
--------
|
||||
.. module:: slixmpp.xmlstream.handler.callback
|
||||
.. module:: slixmpp.xmlstream.handler
|
||||
|
||||
.. autoclass:: Callback
|
||||
:members:
|
||||
|
||||
CoroutineCallback
|
||||
-----------------
|
||||
|
||||
.. autoclass:: CoroutineCallback
|
||||
:members:
|
||||
|
||||
Waiter
|
||||
------
|
||||
.. module:: slixmpp.xmlstream.handler.waiter
|
||||
|
||||
.. autoclass:: Waiter
|
||||
:members:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Jabber IDs (JID)
|
||||
=================
|
||||
|
||||
.. module:: slixmpp.xmlstream.jid
|
||||
.. module:: slixmpp.jid
|
||||
|
||||
.. autoclass:: JID
|
||||
:members:
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
=========
|
||||
Scheduler
|
||||
=========
|
||||
|
||||
.. module:: slixmpp.xmlstream.scheduler
|
||||
|
||||
.. autoclass:: Task
|
||||
:members:
|
||||
|
||||
.. autoclass:: Scheduler
|
||||
:members:
|
||||
@@ -61,8 +61,8 @@ interacting with a given :term:`stanza` a :term:`stanza object`.
|
||||
To make dealing with more complicated and nested :term:`stanzas <stanza>`
|
||||
or XML chunks easier, :term:`stanza objects <stanza object>` can be
|
||||
composed in two ways: as iterable child objects or as plugins. Iterable
|
||||
child stanzas, or :term:`substanzas`, are accessible through a special
|
||||
``'substanzas'`` interface. This option is useful for stanzas which
|
||||
child stanzas, or :term:`substanzas <substanza>`, are accessible through a
|
||||
special ``'substanzas'`` interface. This option is useful for stanzas which
|
||||
may contain more than one of the same kind of element. When there is
|
||||
only one child element, the plugin method is more useful. For plugins,
|
||||
a parent stanza object delegates one of its XML child elements to the
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace because that is already declared by the stream header. But, if
|
||||
you create a :class:`~slixmpp.stanza.message.Message` instance and dump
|
||||
it to the terminal, the ``jabber:client`` namespace will appear.
|
||||
|
||||
.. autofunction:: tostring
|
||||
.. autofunction:: slixmpp.xmlstream.tostring
|
||||
|
||||
Escaping Special Characters
|
||||
---------------------------
|
||||
@@ -43,4 +43,5 @@ In the future, the use of CDATA sections may be allowed to reduce the
|
||||
size of escaped text or for when other XMPP processing agents do not
|
||||
undertand these entities.
|
||||
|
||||
.. autofunction:: xml_escape
|
||||
..
|
||||
autofunction:: xml_escape
|
||||
|
||||
@@ -24,21 +24,20 @@ patterns is received; these callbacks are also referred to as :term:`stream
|
||||
handlers <stream handler>`. The class also provides a basic eventing system
|
||||
which can be triggered either manually or on a timed schedule.
|
||||
|
||||
The Main Threads
|
||||
~~~~~~~~~~~~~~~~
|
||||
:class:`~slixmpp.xmlstream.xmlstream.XMLStream` instances run using at
|
||||
least three background threads: the send thread, the read thread, and the
|
||||
scheduler thread. The send thread is in charge of monitoring the send queue
|
||||
and writing text to the outgoing XML stream. The read thread pulls text off
|
||||
of the incoming XML stream and stores the results in an event queue. The
|
||||
scheduler thread is used to emit events after a given period of time.
|
||||
The event loop
|
||||
~~~~~~~~~~~~~~
|
||||
:class:`~slixmpp.xmlstream.xmlstream.XMLStream` instances inherit the
|
||||
:class:`asyncio.BaseProtocol` class, and therefore do not have to handle
|
||||
reads and writes directly, but receive data through
|
||||
:meth:`~slixmpp.xmlstream.xmlstream.XMLStream.data_received` and write
|
||||
data in the socket transport.
|
||||
|
||||
Additionally, the main event processing loop may be executed in its
|
||||
own thread if Slixmpp is being used in the background for another
|
||||
application.
|
||||
Upon receiving data, :term:`stream handlers <stream handler>` are run
|
||||
immediately, except if they are coroutines, in which case they are
|
||||
scheduled using :meth:`asyncio.async`.
|
||||
|
||||
Short-lived threads may also be spawned as requested for threaded
|
||||
:term:`event handlers <event handler>`.
|
||||
:term:`Event handlers <event handler>` (which are called inside
|
||||
:term:`stream handlers <stream handler>`) work the same way.
|
||||
|
||||
How XML Text is Turned into Action
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -53,7 +52,7 @@ when this bit of XML is received (with an assumed namespace of
|
||||
</message>
|
||||
|
||||
|
||||
1. **Convert XML strings into objects.**
|
||||
#. **Convert XML strings into objects.**
|
||||
|
||||
Incoming text is parsed and converted into XML objects (using
|
||||
ElementTree) which are then wrapped into what are referred to as
|
||||
@@ -66,65 +65,43 @@ when this bit of XML is received (with an assumed namespace of
|
||||
``{jabber:client}message`` is associated with the class
|
||||
:class:`~slixmpp.stanza.Message`.
|
||||
|
||||
2. **Match stanza objects to callbacks.**
|
||||
#. **Match stanza objects to callbacks.**
|
||||
|
||||
These objects are then compared against the stored patterns associated
|
||||
with the registered callback handlers. For each match, a copy of the
|
||||
:term:`stanza object` is paired with a reference to the handler and
|
||||
placed into the event queue.
|
||||
with the registered callback handlers.
|
||||
|
||||
Our :class:`~slixmpp.stanza.Message` object is thus paired with the message stanza handler
|
||||
:meth:`BaseXMPP._handle_message` to create the tuple::
|
||||
Each handler matching our :term:`stanza object` is then added to a list.
|
||||
|
||||
('stanza', stanza_obj, handler)
|
||||
#. **Processing callbacks**
|
||||
|
||||
3. **Process the event queue.**
|
||||
Every handler in the list is then called with the :term:`stanza object`
|
||||
as a parameter; if the handler is a
|
||||
:class:`~slixmpp.xmlstream.handler.CoroutineCallback`
|
||||
then it will be scheduled in the event loop using :meth:`asyncio.async`
|
||||
instead of run.
|
||||
|
||||
The event queue is the heart of Slixmpp. Nearly every action that
|
||||
takes place is first inserted into this queue, whether that be received
|
||||
stanzas, custom events, or scheduled events.
|
||||
|
||||
When the stanza is pulled out of the event queue with an associated
|
||||
callback, the callback function is executed with the stanza as its only
|
||||
parameter.
|
||||
|
||||
.. warning::
|
||||
The callback, aka :term:`stream handler`, is executed in the main event
|
||||
processing thread. If the handler blocks, event processing will also
|
||||
block.
|
||||
|
||||
4. **Raise Custom Events**
|
||||
#. **Raise Custom Events**
|
||||
|
||||
Since a :term:`stream handler` shouldn't block, if extensive processing
|
||||
for a stanza is required (such as needing to send and receive an
|
||||
:class:`~slixmpp.stanza.Iq` stanza), then custom events must be used.
|
||||
These events are not explicitly tied to the incoming XML stream and may
|
||||
be raised at any time. Importantly, these events may be handled in their
|
||||
own thread.
|
||||
be raised at any time.
|
||||
|
||||
When the event is raised, a copy of the stanza is created for each
|
||||
handler registered for the event. In contrast to :term:`stream handlers
|
||||
<stream handler>`, these functions are referred to as :term:`event
|
||||
handlers <event handler>`. Each stanza/handler pair is then put into the
|
||||
event queue.
|
||||
In contrast to :term:`stream handlers <stream handler>`, these functions
|
||||
are referred to as :term:`event handlers <event handler>`.
|
||||
|
||||
The code for :meth:`BaseXMPP._handle_message` follows this pattern, and
|
||||
raises a ``'message'`` event::
|
||||
raises a ``'message'`` event
|
||||
|
||||
self.event('message', msg)
|
||||
.. code-block:: python
|
||||
|
||||
The event call then places the message object back into the event queue
|
||||
paired with an :term:`event handler`::
|
||||
self.event('message', msg)
|
||||
|
||||
('event', 'message', msg_copy1, custom_event_handler_1)
|
||||
('event', 'message', msg_copy2, custom_evetn_handler_2)
|
||||
#. **Process Custom Events**
|
||||
|
||||
5. **Process Custom Events**
|
||||
|
||||
The stanza and :term:`event handler` are then pulled from the event
|
||||
queue, and the handler is executed, passing the stanza as its only
|
||||
argument. If the handler was registered as threaded, then a new thread
|
||||
will be spawned for it.
|
||||
The :term:`event handlers <event handler>` are then executed, passing
|
||||
the stanza as the only argument.
|
||||
|
||||
.. note::
|
||||
Events may be raised without needing :term:`stanza objects <stanza object>`.
|
||||
@@ -135,9 +112,9 @@ when this bit of XML is received (with an assumed namespace of
|
||||
Finally, after a long trek, our message is handed off to the user's
|
||||
custom handler in order to do awesome stuff::
|
||||
|
||||
msg.reply()
|
||||
msg['body'] = "Hey! This is awesome!"
|
||||
msg.send()
|
||||
reply = msg.reply()
|
||||
reply['body'] = "Hey! This is awesome!"
|
||||
reply.send()
|
||||
|
||||
|
||||
.. index:: BaseXMPP, XMLStream
|
||||
|
||||
@@ -48,9 +48,9 @@ copyright = u'2011, Nathan Fritz, Lance Stout'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.0'
|
||||
version = '1.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.0'
|
||||
release = '1.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@@ -105,7 +105,7 @@ html_theme = 'haiku'
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
html_title = 'Slixmpp'
|
||||
html_title = 'slixmpp'
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
html_short_title = '%s Documentation' % release
|
||||
@@ -219,4 +219,4 @@ man_pages = [
|
||||
[u'Nathan Fritz, Lance Stout'], 1)
|
||||
]
|
||||
|
||||
intersphinx_mapping = {'python': ('http://docs.python.org/3.2', 'python-objects.inv')}
|
||||
intersphinx_mapping = {'python': ('http://docs.python.org/3.4', 'python-objects.inv')}
|
||||
|
||||
@@ -634,8 +634,9 @@ with some additional registration fields implemented.
|
||||
if self.backend.register(iq['from'].bare, iq['register']):
|
||||
# Successful registration
|
||||
self.xmpp.event('registered_user', iq)
|
||||
iq.reply().set_payload(iq['register'].xml)
|
||||
iq.send()
|
||||
reply = iq.reply()
|
||||
reply.set_payload(iq['register'].xml)
|
||||
reply.send()
|
||||
else:
|
||||
# Conflicting registration
|
||||
self._sendError(iq, '409', 'cancel', 'conflict',
|
||||
@@ -666,14 +667,16 @@ with some additional registration fields implemented.
|
||||
# Add a blank field
|
||||
reg.addField(field)
|
||||
|
||||
iq.reply().set_payload(reg.xml)
|
||||
iq.send()
|
||||
reply = iq.reply()
|
||||
reply.set_payload(reg.xml)
|
||||
reply.send()
|
||||
|
||||
def _sendError(self, iq, code, error_type, name, text=''):
|
||||
iq.reply().set_payload(iq['register'].xml)
|
||||
iq.error()
|
||||
iq['error']['code'] = code
|
||||
iq['error']['type'] = error_type
|
||||
iq['error']['condition'] = name
|
||||
iq['error']['text'] = text
|
||||
iq.send()
|
||||
reply = iq.reply()
|
||||
reply.set_payload(iq['register'].xml)
|
||||
reply.error()
|
||||
reply['error']['code'] = code
|
||||
reply['error']['type'] = error_type
|
||||
reply['error']['condition'] = name
|
||||
reply['error']['text'] = text
|
||||
reply.send()
|
||||
|
||||
47
docs/differences.rst
Normal file
47
docs/differences.rst
Normal file
@@ -0,0 +1,47 @@
|
||||
.. _differences:
|
||||
|
||||
Differences from SleekXMPP
|
||||
==========================
|
||||
|
||||
**Python 3.4+ only**
|
||||
slixmpp will only work on python 3.4 and above.
|
||||
|
||||
**Stanza copies**
|
||||
The same stanza object is given through all the handlers; a handler that
|
||||
edits the stanza object should make its own copy.
|
||||
|
||||
**Replies**
|
||||
Because stanzas are not copied anymore,
|
||||
:meth:`Stanza.reply() <.StanzaBase.reply>` calls
|
||||
(for :class:`IQs <.Iq>`, :class:`Messages <.Message>`, etc)
|
||||
now return a new object instead of editing the stanza object
|
||||
in-place.
|
||||
|
||||
**Block and threaded arguments**
|
||||
All the functions that had a ``threaded=`` or ``block=`` argument
|
||||
do not have it anymore. Also, :meth:`.Iq.send` **does not block
|
||||
anymore**.
|
||||
|
||||
**Coroutine facilities**
|
||||
**See** :ref:`using_asyncio`
|
||||
|
||||
If an event handler is a coroutine, it will be called asynchronously
|
||||
in the event loop instead of inside the event caller.
|
||||
|
||||
A CoroutineCallback class has been added to create coroutine stream
|
||||
handlers, which will be also handled in the event loop.
|
||||
|
||||
The :class:`~.slixmpp.stanza.Iq` object’s :meth:`~.slixmpp.stanza.Iq.send`
|
||||
method now **always** return a :class:`~.asyncio.Future` which result will be set
|
||||
to the IQ reply when it is received, or to ``None`` if the IQ is not of
|
||||
type ``get`` or ``set``.
|
||||
|
||||
Many plugins (WIP) calls which retrieve information also return the same
|
||||
future.
|
||||
|
||||
**Architectural differences**
|
||||
slixmpp does not have an event queue anymore, and instead processes
|
||||
handlers directly after receiving the XML stanza.
|
||||
|
||||
.. note::
|
||||
If you find something that doesn’t work but should, please report it.
|
||||
@@ -152,6 +152,13 @@ Event Index
|
||||
Makes the contents of message stanzas available whenever one is received. Be
|
||||
sure to check the message type in order to handle error messages.
|
||||
|
||||
message_error
|
||||
- **Data:** :py:class:`~slixmpp.Message`
|
||||
- **Source:** :py:class:`BaseXMPP <slixmpp.BaseXMPP>`
|
||||
|
||||
Makes the contents of message stanzas available whenever one is received.
|
||||
Only handler messages with an ``error`` type.
|
||||
|
||||
message_form
|
||||
- **Data:** :py:class:`~slixmpp.plugins.xep_0004.Form`
|
||||
- **Source:** :py:class:`~slixmpp.plugins.xep_0004.xep_0004`
|
||||
|
||||
@@ -7,19 +7,11 @@ Create and Run a Server Component
|
||||
.. 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/slixmpp-discussion>`_
|
||||
or join the chat room at `sleek@conference.jabber.org
|
||||
<xmpp:sleek@conference.jabber.org?join>`_.
|
||||
join the chat room at `slixmpp@muc.poez.io
|
||||
<xmpp:slixmpp@muc.poez.io?join>`_.
|
||||
|
||||
If you have not yet installed Slixmpp, do so now by either checking out a version
|
||||
from `Github <http://github.com/fritzy/Slixmpp>`_, or installing it using ``pip``
|
||||
or ``easy_install``.
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
pip install slixmpp # Or: easy_install slixmpp
|
||||
|
||||
with `Git <http://git.poez.io/slixmpp>`_.
|
||||
|
||||
Many XMPP applications eventually graduate to requiring to run as a server
|
||||
component in order to meet scalability requirements. To demonstrate how to
|
||||
|
||||
@@ -7,19 +7,11 @@ Slixmpp Quickstart - Echo 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/slixmpp-discussion>`_
|
||||
or join the chat room at `sleek@conference.jabber.org
|
||||
<xmpp:sleek@conference.jabber.org?join>`_.
|
||||
join the chat room at `slixmpp@muc.poez.io
|
||||
<xmpp:slixmpp@muc.poez.io?join>`_.
|
||||
|
||||
If you have not yet installed Slixmpp, do so now by either checking out a version
|
||||
from `Github <http://github.com/fritzy/Slixmpp>`_, or installing it using ``pip``
|
||||
or ``easy_install``.
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
pip install slixmpp # Or: easy_install slixmpp
|
||||
|
||||
with `Git <http://git.poez.io/slixmpp>`_.
|
||||
|
||||
As a basic starting project, we will create an echo bot which will reply to any
|
||||
messages sent to it. We will also go through adding some basic command line configuration
|
||||
@@ -44,6 +36,7 @@ To get started, here is a brief outline of the structure that the final project
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import asyncio
|
||||
import logging
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
@@ -59,24 +52,6 @@ To get started, here is a brief outline of the structure that the final project
|
||||
|
||||
'''Finally, we connect the bot and start listening for messages'''
|
||||
|
||||
Default Encoding
|
||||
----------------
|
||||
XMPP requires support for UTF-8 and so Slixmpp must use UTF-8 as well. In
|
||||
Python3 this is simple because Unicode is the default string type. For Python2.6+
|
||||
the situation is not as easy because standard strings are simply byte arrays and
|
||||
use ASCII. We can get Python to use UTF-8 as the default encoding by including:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
from slixmpp.util.misc_ops import setdefaultencoding
|
||||
setdefaultencoding('utf8')
|
||||
|
||||
.. warning::
|
||||
|
||||
Until we are able to ensure that Slixmpp will always use Unicode in Python2.6+, this
|
||||
may cause issues embedding Slixmpp into other applications which assume ASCII encoding.
|
||||
|
||||
Creating the EchoBot Class
|
||||
--------------------------
|
||||
|
||||
@@ -313,9 +288,9 @@ the ``EchoBot.__init__`` method instead.
|
||||
xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||
|
||||
Now we're ready to connect and begin echoing messages. If you have the package
|
||||
``dnspython`` installed, then the :meth:`slixmpp.clientxmpp.ClientXMPP` method
|
||||
``aiodns`` installed, then the :meth:`slixmpp.clientxmpp.ClientXMPP` method
|
||||
will perform a DNS query to find the appropriate server to connect to for the
|
||||
given JID. If you do not have ``dnspython``, then Slixmpp will attempt to
|
||||
given JID. If you do not have ``aiodns``, then Slixmpp will attempt to
|
||||
connect to the hostname used by the JID, unless an address tuple is supplied
|
||||
to :meth:`slixmpp.clientxmpp.ClientXMPP`.
|
||||
|
||||
@@ -330,22 +305,6 @@ to :meth:`slixmpp.clientxmpp.ClientXMPP`.
|
||||
else:
|
||||
print('Unable to connect')
|
||||
|
||||
.. note::
|
||||
|
||||
For Google Talk users withouth ``dnspython`` installed, the above code
|
||||
should look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# .. option parsing & echo bot configuration
|
||||
|
||||
if xmpp.connect(('talk.google.com', 5222)):
|
||||
xmpp.process(block=True)
|
||||
else:
|
||||
print('Unable to connect')
|
||||
|
||||
To begin responding to messages, you'll see we called :meth:`slixmpp.basexmpp.BaseXMPP.process`
|
||||
which will start the event handling, send queue, and XML reader threads. It will also call
|
||||
the :meth:`slixmpp.plugins.base.BasePlugin.post_init` method on all registered plugins. By
|
||||
|
||||
@@ -7,19 +7,11 @@ 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/slixmpp-discussion>`_
|
||||
or join the chat room at `sleek@conference.jabber.org
|
||||
<xmpp:sleek@conference.jabber.org?join>`_.
|
||||
join the chat room at `slixmpp@muc.poez.io
|
||||
<xmpp:slixmpp@muc.poez.io?join>`_.
|
||||
|
||||
If you have not yet installed Slixmpp, do so now by either checking out a version
|
||||
from `Github <http://github.com/fritzy/Slixmpp>`_, or installing it using ``pip``
|
||||
or ``easy_install``.
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
pip install slixmpp # Or: easy_install slixmpp
|
||||
|
||||
from `Git <http://git.poez.io/slixmpp>`_.
|
||||
|
||||
Now that you've got the basic gist of using Slixmpp by following the
|
||||
echobot example (:ref:`echobot`), we can use one of the bundled plugins
|
||||
|
||||
@@ -7,10 +7,8 @@ Enable HTTP Proxy Support
|
||||
.. 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/slixmpp-discussion>`_
|
||||
or join the chat room at `sleek@conference.jabber.org
|
||||
<xmpp:sleek@conference.jabber.org?join>`_.
|
||||
join the chat room at `slixmpp@muc.poez.io
|
||||
<xmpp:slixmpp@muc.poez.io?join>`_.
|
||||
|
||||
In some instances, you may wish to route XMPP traffic through
|
||||
an HTTP proxy, probably to get around restrictive firewalls.
|
||||
|
||||
@@ -4,10 +4,8 @@ Sign in, Send a Message, and Disconnect
|
||||
.. 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/slixmpp-discussion>`_
|
||||
or join the chat room at `sleek@conference.jabber.org
|
||||
<xmpp:sleek@conference.jabber.org?join>`_.
|
||||
join the chat room at `slixmpp@muc.poez.io
|
||||
<xmpp:slixmpp@muc.poez.io?join>`_.
|
||||
|
||||
A common use case for Slixmpp is to send one-off messages from
|
||||
time to time. For example, one use case could be sending out a notice when
|
||||
|
||||
@@ -9,21 +9,20 @@ Glossary
|
||||
stream handler
|
||||
A callback function that accepts stanza objects pulled directly
|
||||
from the XML stream. A stream handler is encapsulated in a
|
||||
object that includes a :term:`Matcher` object, and which provides
|
||||
additional semantics. For example, the ``Waiter`` handler wrapper
|
||||
blocks thread execution until a matching stanza is received.
|
||||
object that includes a :class:`Matcher <.MatcherBase>` object, and
|
||||
which provides additional semantics. For example, the
|
||||
:class:`.Waiter` handler wrapper blocks thread execution until a
|
||||
matching stanza is received.
|
||||
|
||||
event handler
|
||||
A callback function that responds to events raised by
|
||||
``XMLStream.event``. An event handler may be marked as
|
||||
threaded, allowing it to execute outside of the main processing
|
||||
loop.
|
||||
:meth:`.XMLStream.event`.
|
||||
|
||||
stanza object
|
||||
Informally may refer both to classes which extend ``ElementBase``
|
||||
or ``StanzaBase``, and to objects of such classes.
|
||||
Informally may refer both to classes which extend :class:`.ElementBase`
|
||||
or :class:`.StanzaBase`, and to objects of such classes.
|
||||
|
||||
A stanza object is a wrapper for an XML object which exposes ``dict``
|
||||
A stanza object is a wrapper for an XML object which exposes :class:`dict`
|
||||
like interfaces which may be assigned to, read from, or deleted.
|
||||
|
||||
stanza plugin
|
||||
|
||||
@@ -161,8 +161,8 @@ item itself, and the JID and node that will own the item.
|
||||
In this case, the owning JID and node are provided with the
|
||||
parameters ``ijid`` and ``node``.
|
||||
|
||||
Peforming Disco Queries
|
||||
-----------------------
|
||||
Performing Disco Queries
|
||||
------------------------
|
||||
The methods ``get_info()`` and ``get_items()`` are used to query remote JIDs
|
||||
and their nodes for disco information. Since these methods are wrappers for
|
||||
sending Iq stanzas, they also accept all of the parameters of the ``Iq.send()``
|
||||
@@ -172,11 +172,10 @@ the `XEP-0059 <http://xmpp.org/extensions/xep-0059.html>`_ plug-in.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
info = self['xep_0030'].get_info(jid='foo@example.com',
|
||||
node='bar',
|
||||
ifrom='baz@mycomponent.example.com',
|
||||
block=True,
|
||||
timeout=30)
|
||||
info = yield from self['xep_0030'].get_info(jid='foo@example.com',
|
||||
node='bar',
|
||||
ifrom='baz@mycomponent.example.com',
|
||||
timeout=30)
|
||||
|
||||
items = self['xep_0030'].get_info(jid='foo@example.com',
|
||||
node='bar',
|
||||
|
||||
@@ -3,38 +3,25 @@ Slixmpp
|
||||
|
||||
.. sidebar:: Get the Code
|
||||
|
||||
.. code-block:: sh
|
||||
The latest source code for Slixmpp may be found on the `Git repo
|
||||
<http://git.poez.io/slixmpp>`_. ::
|
||||
|
||||
pip install slixmpp
|
||||
git clone git://git.poez.io/slixmpp
|
||||
|
||||
The latest source code for Slixmpp may be found on `Github
|
||||
<http://github.com/fritzy/Slixmpp>`_. Releases can be found in the
|
||||
``master`` branch, while the latest development version is in the
|
||||
``develop`` branch.
|
||||
|
||||
**Latest Stable Release**
|
||||
- `1.0 <http://github.com/fritzy/Slixmpp/zipball/1.0>`_
|
||||
|
||||
**Develop Releases**
|
||||
- `Latest Develop Version <http://github.com/fritzy/Slixmpp/zipball/develop>`_
|
||||
|
||||
|
||||
A mailing list and XMPP chat room are available for discussing and getting
|
||||
help with Slixmpp.
|
||||
|
||||
**Mailing List**
|
||||
`Slixmpp Discussion on Google Groups <http://groups.google.com/group/slixmpp-discussion>`_
|
||||
An XMPP chat room is available for discussing and getting help with slixmpp.
|
||||
|
||||
**Chat**
|
||||
`sleek@conference.jabber.org <xmpp:sleek@conference.jabber.org?join>`_
|
||||
`slixmpp@muc.poez.io <xmpp:slixmpp@muc.poez.io?join>`_
|
||||
|
||||
**Reporting bugs**
|
||||
You can report bugs at http://dev.louiz.org/projects/slixmpp/issues.
|
||||
|
||||
Slixmpp is an :ref:`MIT licensed <license>` XMPP library for Python 2.6/3.1+,
|
||||
and is featured in examples in
|
||||
`XMPP: The Definitive Guide <http://oreilly.com/catalog/9780596521271>`_
|
||||
by Kevin Smith, Remko Tronçon, and Peter Saint-Andre. If you've arrived
|
||||
here from reading the Definitive Guide, please see the notes on updating
|
||||
the examples to the latest version of Slixmpp.
|
||||
.. note::
|
||||
slixmpp is a friendly fork of `SleekXMPP <https://github.com/fritzy/SleekXMPP>`_
|
||||
which goal is to use asyncio instead of threads to handle networking. See
|
||||
:ref:`differences`.
|
||||
|
||||
Slixmpp is an :ref:`MIT licensed <license>` XMPP library for Python 3.4+,
|
||||
|
||||
Slixmpp's design goals and philosphy are:
|
||||
|
||||
@@ -59,15 +46,16 @@ Slixmpp'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 Slixmpp Bot:
|
||||
--------------------------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from slixmpp import ClientXMPP
|
||||
from slixmpp.exceptions import IqError, IqTimeout
|
||||
|
||||
|
||||
class EchoBot(ClientXMPP):
|
||||
@@ -85,27 +73,13 @@ Here's your first Slixmpp Bot:
|
||||
# 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()
|
||||
# are sent asynchronously. You can almost always provide a
|
||||
# callback that will be executed when the reply is received.
|
||||
|
||||
def message(self, msg):
|
||||
if msg['type'] in ('chat', 'normal'):
|
||||
@@ -121,9 +95,18 @@ Here's your first Slixmpp Bot:
|
||||
|
||||
xmpp = EchoBot('somejid@example.com', 'use_getpass')
|
||||
xmpp.connect()
|
||||
xmpp.process(block=True)
|
||||
xmpp.process()
|
||||
|
||||
|
||||
To read if you come from SleekXMPP
|
||||
----------------------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
differences
|
||||
using_asyncio
|
||||
|
||||
|
||||
Getting Started (with Examples)
|
||||
-------------------------------
|
||||
@@ -145,7 +128,6 @@ Tutorials, FAQs, and How To Guides
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
faq
|
||||
xeps
|
||||
xmpp_tdg
|
||||
howto/stanzas
|
||||
@@ -184,9 +166,7 @@ API Reference
|
||||
api/xmlstream/handler
|
||||
api/xmlstream/matcher
|
||||
api/xmlstream/xmlstream
|
||||
api/xmlstream/scheduler
|
||||
api/xmlstream/tostring
|
||||
api/xmlstream/filesocket
|
||||
|
||||
Core Stanzas
|
||||
~~~~~~~~~~~~
|
||||
@@ -197,8 +177,6 @@ Core Stanzas
|
||||
api/stanza/message
|
||||
api/stanza/presence
|
||||
api/stanza/iq
|
||||
api/stanza/error
|
||||
api/stanza/stream_error
|
||||
|
||||
Plugins
|
||||
~~~~~~~
|
||||
@@ -220,8 +198,14 @@ Additional Info
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
Credits
|
||||
-------
|
||||
SleekXMPP Credits
|
||||
-----------------
|
||||
|
||||
.. note::
|
||||
Those people made SleekXMPP, so you should not bother them if
|
||||
you have an issue with slixmpp. But it’s still fair to credit
|
||||
them for their work.
|
||||
|
||||
|
||||
**Main Author:** `Nathan Fritz <http://andyet.net/team/fritzy>`_
|
||||
`fritzy@netflint.net <xmpp:fritzy@netflint.net?message>`_,
|
||||
|
||||
148
docs/using_asyncio.rst
Normal file
148
docs/using_asyncio.rst
Normal file
@@ -0,0 +1,148 @@
|
||||
.. _using_asyncio:
|
||||
|
||||
=============
|
||||
Using asyncio
|
||||
=============
|
||||
|
||||
Block on IQ sending
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:meth:`.Iq.send` now returns a :class:`~.Future` so you can easily block with:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
result = yield from iq.send()
|
||||
|
||||
.. warning::
|
||||
|
||||
If the reply is an IQ with an ``error`` type, this will raise an
|
||||
:class:`.IqError`, and if it timeouts, it will raise an
|
||||
:class:`.IqTimeout`. Don't forget to catch it.
|
||||
|
||||
You can still use callbacks instead.
|
||||
|
||||
XEP plugin integration
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The same changes from the SleekXMPP API apply, so you can do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
iq_info = yield from self.xmpp['xep_0030'].get_info(jid)
|
||||
|
||||
But the following will only return a Future:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
iq_info = self.xmpp['xep_0030'].get_info(jid)
|
||||
|
||||
|
||||
Callbacks, Event Handlers, and Stream Handlers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
IQ callbacks and :term:`Event Handlers <event handler>` can be coroutine
|
||||
functions; in this case, they will be scheduled in the event loop using
|
||||
:meth:`.asyncio.async` and not ran immediately.
|
||||
|
||||
A :class:`.CoroutineCallback` class has been added as well for
|
||||
:term:`Stream Handlers <stream handler>`, which will use
|
||||
:meth:`.asyncio.async` to schedule the callback.
|
||||
|
||||
Running the event loop
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:meth:`.XMLStream.process` is only a thin wrapper on top of
|
||||
``loop.run_forever()`` (if ``timeout`` is provided then it will
|
||||
only run for this amount of time, and if ``forever`` is False it will
|
||||
run until disconnection).
|
||||
|
||||
Therefore you can handle the event loop in any way you like
|
||||
instead of using ``process()``.
|
||||
|
||||
|
||||
Examples
|
||||
~~~~~~~~
|
||||
|
||||
Blocking until the session is established
|
||||
-----------------------------------------
|
||||
|
||||
This code blocks until the XMPP session is fully established, which
|
||||
can be useful to make sure external events aren’t triggering XMPP
|
||||
callbacks while everything is not ready.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio, slixmpp
|
||||
|
||||
client = slixmpp.ClientXMPP('jid@example', 'password')
|
||||
client.connected_event = asyncio.Event()
|
||||
callback = lambda _: client.connected_event.set()
|
||||
client.add_event_handler('session_start', callback)
|
||||
client.connect()
|
||||
loop.run_until_complete(event.wait())
|
||||
# do some other stuff before running the event loop, e.g.
|
||||
# loop.run_until_complete(httpserver.init())
|
||||
client.process()
|
||||
|
||||
|
||||
Use with other asyncio-based libraries
|
||||
--------------------------------------
|
||||
|
||||
This code interfaces with aiohttp to retrieve two pages asynchronously
|
||||
when the session is established, and then send the HTML content inside
|
||||
a simple <message>.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio, aiohttp, slixmpp
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_pythonorg(event):
|
||||
req = yield from aiohttp.request('get', 'http://www.python.org')
|
||||
text = yield from req.text
|
||||
client.send_message(mto='jid2@example', mbody=text)
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_asyncioorg(event):
|
||||
req = yield from aiohttp.request('get', 'http://www.asyncio.org')
|
||||
text = yield from req.text
|
||||
client.send_message(mto='jid3@example', mbody=text)
|
||||
|
||||
client = slixmpp.ClientXMPP('jid@example', 'password')
|
||||
client.add_event_handler('session_start', get_pythonorg)
|
||||
client.add_event_handler('session_start', get_asyncioorg)
|
||||
client.connect()
|
||||
client.process()
|
||||
|
||||
|
||||
Blocking Iq
|
||||
-----------
|
||||
|
||||
This client checks (via XEP-0092) the software used by every entity it
|
||||
receives a message from. After this, it sends a message to a specific
|
||||
JID indicating its findings.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio, slixmpp
|
||||
|
||||
class ExampleClient(slixmpp.ClientXMPP):
|
||||
def __init__(self, *args, **kwargs):
|
||||
slixmpp.ClientXMPP.__init__(self, *args, **kwargs)
|
||||
self.register_plugin('xep_0092')
|
||||
self.add_event_handler('message', self.on_message)
|
||||
|
||||
@asyncio.coroutine
|
||||
def on_message(self, event):
|
||||
# You should probably handle IqError and IqTimeout exceptions here
|
||||
# but this is an example.
|
||||
version = yield from self['xep_0092'].get_version(message['from'])
|
||||
text = "%s sent me a message, he runs %s" % (message['from'],
|
||||
version['software_version']['name'])
|
||||
self.send_message(mto='master@example.tld', mbody=text)
|
||||
|
||||
client = ExampleClient('jid@example', 'password')
|
||||
client.connect()
|
||||
client.process()
|
||||
|
||||
|
||||
@@ -11,20 +11,12 @@
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
# This can be used when you are in a test environment and need to make paths right
|
||||
sys.path=['/Users/jocke/Dropbox/06_dev/Slixmpp']+sys.path
|
||||
|
||||
import logging
|
||||
import unittest
|
||||
import distutils.core
|
||||
import datetime
|
||||
|
||||
from glob import glob
|
||||
from os.path import splitext, basename, join as pjoin
|
||||
from os.path import basename, join as pjoin
|
||||
from argparse import ArgumentParser
|
||||
from urllib import urlopen
|
||||
from getpass import getpass
|
||||
|
||||
import slixmpp
|
||||
from slixmpp.plugins.xep_0323.device import Device
|
||||
@@ -168,9 +160,9 @@ if __name__ == '__main__':
|
||||
|
||||
myDevice = TheDevice(args.nodeid);
|
||||
# myDevice._add_field(name="Relay", typename="numeric", unit="Bool");
|
||||
myDevice._add_field(name="Temperature", typename="numeric", unit="C");
|
||||
myDevice._add_field(name="Temperature", typename="numeric", unit="C")
|
||||
myDevice._set_momentary_timestamp("2013-03-07T16:24:30")
|
||||
myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"});
|
||||
myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"})
|
||||
|
||||
xmpp['xep_0323'].register_node(nodeId=args.nodeid, device=myDevice, commTimeout=10);
|
||||
xmpp.beClientOrServer(server=True)
|
||||
@@ -186,5 +178,5 @@ if __name__ == '__main__':
|
||||
logging.debug("ready ending")
|
||||
|
||||
else:
|
||||
print "noopp didn't happen"
|
||||
print("noopp didn't happen")
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from argparse import ArgumentParser
|
||||
|
||||
import slixmpp
|
||||
from slixmpp.exceptions import IqError, IqTimeout
|
||||
from slixmpp.xmlstream.asyncio import asyncio
|
||||
|
||||
|
||||
class Disco(slixmpp.ClientXMPP):
|
||||
@@ -53,6 +54,7 @@ class Disco(slixmpp.ClientXMPP):
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
@@ -74,22 +76,16 @@ class Disco(slixmpp.ClientXMPP):
|
||||
|
||||
try:
|
||||
if self.get in self.info_types:
|
||||
# By using block=True, the result stanza will be
|
||||
# returned. Execution will block until the reply is
|
||||
# received. Non-blocking options would be to listen
|
||||
# for the disco_info event, or passing a handler
|
||||
# function using the callback parameter.
|
||||
info = self['xep_0030'].get_info(jid=self.target_jid,
|
||||
node=self.target_node,
|
||||
block=True)
|
||||
elif self.get in self.items_types:
|
||||
info = yield from self['xep_0030'].get_info(jid=self.target_jid,
|
||||
node=self.target_node)
|
||||
if self.get in self.items_types:
|
||||
# The same applies from above. Listen for the
|
||||
# disco_items event or pass a callback function
|
||||
# if you need to process a non-blocking request.
|
||||
items = self['xep_0030'].get_items(jid=self.target_jid,
|
||||
node=self.target_node,
|
||||
block=True)
|
||||
else:
|
||||
items = yield from self['xep_0030'].get_items(jid=self.target_jid,
|
||||
node=self.target_node)
|
||||
if self.get not in self.info_types and self.get not in self.items_types:
|
||||
logging.error("Invalid disco request type.")
|
||||
return
|
||||
except IqError as e:
|
||||
@@ -143,7 +139,7 @@ if __name__ == '__main__':
|
||||
parser.add_argument("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
parser.add_argument("query", choices=["all", "info", "items", "identities", "features"])
|
||||
parser.add_argument("target-jid")
|
||||
parser.add_argument("target_jid")
|
||||
parser.add_argument("node", nargs='?')
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -162,4 +158,4 @@ if __name__ == '__main__':
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
xmpp.connect()
|
||||
xmpp.process()
|
||||
xmpp.process(forever=False)
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
|
||||
import logging
|
||||
from getpass import getpass
|
||||
import threading
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import slixmpp
|
||||
from slixmpp.exceptions import XMPPError
|
||||
from slixmpp import asyncio
|
||||
|
||||
|
||||
FILE_TYPES = {
|
||||
@@ -40,8 +40,14 @@ class AvatarDownloader(slixmpp.ClientXMPP):
|
||||
self.add_event_handler('avatar_metadata_publish', self.on_avatar)
|
||||
|
||||
self.received = set()
|
||||
self.presences_received = threading.Event()
|
||||
self.presences_received = asyncio.Event()
|
||||
self.roster_received = asyncio.Event()
|
||||
|
||||
def roster_received_cb(self, event):
|
||||
self.roster_received.set()
|
||||
self.presences_received.clear()
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
@@ -56,16 +62,20 @@ class AvatarDownloader(slixmpp.ClientXMPP):
|
||||
data.
|
||||
"""
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
self.get_roster(callback=self.roster_received_cb)
|
||||
|
||||
print('Waiting for presence updates...\n')
|
||||
self.presences_received.wait(15)
|
||||
yield from self.roster_received.wait()
|
||||
print('Roster received')
|
||||
yield from self.presences_received.wait()
|
||||
self.disconnect()
|
||||
|
||||
@asyncio.coroutine
|
||||
def on_vcard_avatar(self, pres):
|
||||
print("Received vCard avatar update from %s" % pres['from'].bare)
|
||||
try:
|
||||
result = self['xep_0054'].get_vcard(pres['from'], cached=True)
|
||||
result = yield from self['xep_0054'].get_vcard(pres['from'].bare, cached=True,
|
||||
timeout=5)
|
||||
except XMPPError:
|
||||
print("Error retrieving avatar for %s" % pres['from'])
|
||||
return
|
||||
@@ -76,16 +86,18 @@ class AvatarDownloader(slixmpp.ClientXMPP):
|
||||
pres['from'].bare,
|
||||
pres['vcard_temp_update']['photo'],
|
||||
filetype)
|
||||
with open(filename, 'w+') as img:
|
||||
with open(filename, 'wb+') as img:
|
||||
img.write(avatar['BINVAL'])
|
||||
|
||||
@asyncio.coroutine
|
||||
def on_avatar(self, msg):
|
||||
print("Received avatar update from %s" % msg['from'])
|
||||
metadata = msg['pubsub_event']['items']['item']['avatar_metadata']
|
||||
for info in metadata['items']:
|
||||
if not info['url']:
|
||||
try:
|
||||
result = self['xep_0084'].retrieve_avatar(msg['from'], info['id'])
|
||||
result = yield from self['xep_0084'].retrieve_avatar(msg['from'].bare, info['id'],
|
||||
timeout=5)
|
||||
except XMPPError:
|
||||
print("Error retrieving avatar for %s" % msg['from'])
|
||||
return
|
||||
@@ -94,7 +106,7 @@ class AvatarDownloader(slixmpp.ClientXMPP):
|
||||
|
||||
filetype = FILE_TYPES.get(metadata['type'], 'png')
|
||||
filename = 'avatar_%s_%s.%s' % (msg['from'].bare, info['id'], filetype)
|
||||
with open(filename, 'w+') as img:
|
||||
with open(filename, 'wb+') as img:
|
||||
img.write(avatar['value'])
|
||||
else:
|
||||
# We could retrieve the avatar via HTTP, etc here instead.
|
||||
@@ -105,6 +117,7 @@ class AvatarDownloader(slixmpp.ClientXMPP):
|
||||
Wait to receive updates from all roster contacts.
|
||||
"""
|
||||
self.received.add(pres['from'].bare)
|
||||
print((len(self.received), len(self.client_roster.keys())))
|
||||
if len(self.received) >= len(self.client_roster.keys()):
|
||||
self.presences_received.set()
|
||||
else:
|
||||
|
||||
97
examples/http_over_xmpp.py
Normal file
97
examples/http_over_xmpp.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Implementation of HTTP over XMPP transport
|
||||
http://xmpp.org/extensions/xep-0332.html
|
||||
Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
|
||||
This file is part of slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from slixmpp import ClientXMPP
|
||||
|
||||
from optparse import OptionParser
|
||||
import logging
|
||||
import getpass
|
||||
|
||||
|
||||
class HTTPOverXMPPClient(ClientXMPP):
|
||||
def __init__(self, jid, password):
|
||||
ClientXMPP.__init__(self, jid, password)
|
||||
self.register_plugin('xep_0332') # HTTP over XMPP Transport
|
||||
self.add_event_handler(
|
||||
'session_start', self.session_start, threaded=True
|
||||
)
|
||||
self.add_event_handler('http_request', self.http_request_received)
|
||||
self.add_event_handler('http_response', self.http_response_received)
|
||||
|
||||
def http_request_received(self, iq):
|
||||
pass
|
||||
|
||||
def http_response_received(self, iq):
|
||||
print('HTTP Response Received : %s' % iq)
|
||||
print('From : %s' % iq['from'])
|
||||
print('To : %s' % iq['to'])
|
||||
print('Type : %s' % iq['type'])
|
||||
print('Headers : %s' % iq['resp']['headers'])
|
||||
print('Code : %s' % iq['resp']['code'])
|
||||
print('Message : %s' % iq['resp']['message'])
|
||||
print('Data : %s' % iq['resp']['data'])
|
||||
|
||||
def session_start(self, event):
|
||||
# TODO: Fill in the blanks
|
||||
self['xep_0332'].send_request(
|
||||
to='?', method='?', resource='?', headers={}
|
||||
)
|
||||
self.disconnect()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
#
|
||||
# NOTE: To run this example, fill up the blanks in session_start() and
|
||||
# use the following command.
|
||||
#
|
||||
# ./http_over_xmpp.py -J <jid> -P <pwd> -i <ip> -p <port> [-v]
|
||||
#
|
||||
|
||||
parser = OptionParser()
|
||||
|
||||
# Output verbosity options.
|
||||
parser.add_option(
|
||||
'-v', '--verbose', help='set logging to DEBUG', action='store_const',
|
||||
dest='loglevel', const=logging.DEBUG, default=logging.ERROR
|
||||
)
|
||||
|
||||
# JID and password options.
|
||||
parser.add_option('-J', '--jid', dest='jid', help='JID')
|
||||
parser.add_option('-P', '--password', dest='password', help='Password')
|
||||
|
||||
# XMPP server ip and port options.
|
||||
parser.add_option(
|
||||
'-i', '--ipaddr', dest='ipaddr',
|
||||
help='IP Address of the XMPP server', default=None
|
||||
)
|
||||
parser.add_option(
|
||||
'-p', '--port', dest='port',
|
||||
help='Port of the XMPP server', default=None
|
||||
)
|
||||
|
||||
opts, args = parser.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if opts.jid is None:
|
||||
opts.jid = input('Username: ')
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass('Password: ')
|
||||
|
||||
xmpp = HTTPOverXMPPClient(opts.jid, opts.password)
|
||||
xmpp.connect()
|
||||
xmpp.process()
|
||||
|
||||
@@ -22,13 +22,10 @@ class IBBReceiver(slixmpp.ClientXMPP):
|
||||
A basic example of creating and using an in-band bytestream.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password):
|
||||
def __init__(self, jid, password, filename):
|
||||
slixmpp.ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
self.register_plugin('xep_0030') # Service Discovery
|
||||
self.register_plugin('xep_0047', {
|
||||
'auto_accept': True
|
||||
}) # In-band Bytestreams
|
||||
self.file = open(filename, 'wb')
|
||||
|
||||
# The session_start event will be triggered when
|
||||
# the bot establishes its connection with the server
|
||||
@@ -39,6 +36,7 @@ class IBBReceiver(slixmpp.ClientXMPP):
|
||||
|
||||
self.add_event_handler("ibb_stream_start", self.stream_opened)
|
||||
self.add_event_handler("ibb_stream_data", self.stream_data)
|
||||
self.add_event_handler("ibb_stream_end", self.stream_closed)
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
@@ -56,29 +54,16 @@ class IBBReceiver(slixmpp.ClientXMPP):
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
|
||||
def accept_stream(self, iq):
|
||||
"""
|
||||
Check that it is ok to accept a stream request.
|
||||
|
||||
Controlling stream acceptance can be done via either:
|
||||
- setting 'auto_accept' to False in the plugin
|
||||
configuration. The default is True.
|
||||
- setting 'accept_stream' to a function which accepts
|
||||
an Iq stanza as its argument, like this one.
|
||||
|
||||
The accept_stream function will be used if it exists, and the
|
||||
auto_accept value will be used otherwise.
|
||||
"""
|
||||
return True
|
||||
|
||||
def stream_opened(self, stream):
|
||||
print('Stream opened: %s from %s' % (stream.sid, stream.peer_jid))
|
||||
|
||||
# You could run a loop reading from the stream using stream.recv(),
|
||||
# or use the ibb_stream_data event.
|
||||
def stream_data(self, stream):
|
||||
self.file.write(stream.read())
|
||||
|
||||
def stream_data(self, event):
|
||||
print(event['data'])
|
||||
def stream_closed(self, stream):
|
||||
print('Stream closed: %s from %s' % (stream.sid, stream.peer_jid))
|
||||
self.file.close()
|
||||
self.disconnect()
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
@@ -97,6 +82,8 @@ if __name__ == '__main__':
|
||||
help="JID to use")
|
||||
parser.add_argument("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
parser.add_argument("-o", "--out", dest="filename",
|
||||
help="file to save to")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -108,9 +95,18 @@ if __name__ == '__main__':
|
||||
args.jid = input("Username: ")
|
||||
if args.password is None:
|
||||
args.password = getpass("Password: ")
|
||||
if args.filename is None:
|
||||
args.filename = input("File path: ")
|
||||
|
||||
xmpp = IBBReceiver(args.jid, args.password)
|
||||
# Setup the IBBReceiver and register plugins. Note that while plugins may
|
||||
# have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = IBBReceiver(args.jid, args.password, args.filename)
|
||||
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||
xmpp.register_plugin('xep_0047', {
|
||||
'auto_accept': True
|
||||
}) # In-band Bytestreams
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
xmpp.connect()
|
||||
xmpp.process()
|
||||
xmpp.process(forever=False)
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from getpass import getpass
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import slixmpp
|
||||
from slixmpp.exceptions import IqError, IqTimeout
|
||||
|
||||
|
||||
class IBBSender(slixmpp.ClientXMPP):
|
||||
@@ -22,11 +24,13 @@ class IBBSender(slixmpp.ClientXMPP):
|
||||
A basic example of creating and using an in-band bytestream.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, receiver, filename):
|
||||
def __init__(self, jid, password, receiver, filename, use_messages=False):
|
||||
slixmpp.ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
self.receiver = receiver
|
||||
self.filename = filename
|
||||
|
||||
self.file = open(filename, 'rb')
|
||||
self.use_messages = use_messages
|
||||
|
||||
# The session_start event will be triggered when
|
||||
# the bot establishes its connection with the server
|
||||
@@ -35,6 +39,7 @@ class IBBSender(slixmpp.ClientXMPP):
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
@@ -51,15 +56,22 @@ class IBBSender(slixmpp.ClientXMPP):
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
|
||||
# For the purpose of demonstration, we'll set a very small block
|
||||
# size. The default block size is 4096. We'll also use a window
|
||||
# allowing sending multiple blocks at a time; in this case, three
|
||||
# block transfers may be in progress at any time.
|
||||
stream = self['xep_0047'].open_stream(self.receiver)
|
||||
try:
|
||||
# Open the IBB stream in which to write to.
|
||||
stream = yield from self['xep_0047'].open_stream(self.receiver, use_messages=self.use_messages)
|
||||
|
||||
with open(self.filename) as f:
|
||||
data = f.read()
|
||||
stream.sendall(data)
|
||||
# If you want to send in-memory bytes, use stream.sendall() instead.
|
||||
yield from stream.sendfile(self.file, timeout=10)
|
||||
|
||||
# And finally close the stream.
|
||||
yield from stream.close(timeout=10)
|
||||
except (IqError, IqTimeout):
|
||||
print('File transfer errored')
|
||||
else:
|
||||
print('File transfer finished')
|
||||
finally:
|
||||
self.file.close()
|
||||
self.disconnect()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -80,9 +92,11 @@ if __name__ == '__main__':
|
||||
parser.add_argument("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
parser.add_argument("-r", "--receiver", dest="receiver",
|
||||
help="JID to use")
|
||||
help="JID of the receiver")
|
||||
parser.add_argument("-f", "--file", dest="filename",
|
||||
help="JID to use")
|
||||
help="file to send")
|
||||
parser.add_argument("-m", "--use-messages", action="store_true",
|
||||
help="use messages instead of iqs for file transfer")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -99,16 +113,13 @@ if __name__ == '__main__':
|
||||
if args.filename is None:
|
||||
args.filename = input("File path: ")
|
||||
|
||||
# Setup the EchoBot and register plugins. Note that while plugins may
|
||||
# Setup the IBBSender and register plugins. Note that while plugins may
|
||||
# have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = IBBSender(args.jid, args.password, args.receiver, args.filename)
|
||||
xmpp = IBBSender(args.jid, args.password, args.receiver, args.filename, args.use_messages)
|
||||
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||
xmpp.register_plugin('xep_0004') # Data Forms
|
||||
xmpp.register_plugin('xep_0047') # In-band Bytestreams
|
||||
xmpp.register_plugin('xep_0060') # PubSub
|
||||
xmpp.register_plugin('xep_0199') # XMPP Ping
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
xmpp.connect()
|
||||
xmpp.process()
|
||||
xmpp.process(forever=False)
|
||||
|
||||
@@ -100,8 +100,8 @@ def on_session2(event):
|
||||
new_xmpp.update_roster(jid,
|
||||
name = item['name'],
|
||||
groups = item['groups'])
|
||||
new_xmpp.disconnect()
|
||||
new_xmpp.disconnect()
|
||||
new_xmpp.add_event_handler('session_start', on_session2)
|
||||
|
||||
if new_xmpp.connect():
|
||||
new_xmpp.process(block=True)
|
||||
new_xmpp.connect()
|
||||
new_xmpp.process()
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
import logging
|
||||
from getpass import getpass
|
||||
from argparse import ArgumentParser
|
||||
from slixmpp.exceptions import IqError, IqTimeout
|
||||
from slixmpp import asyncio
|
||||
|
||||
import slixmpp
|
||||
|
||||
@@ -36,6 +38,7 @@ class PingTest(slixmpp.ClientXMPP):
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
@@ -53,8 +56,8 @@ class PingTest(slixmpp.ClientXMPP):
|
||||
self.get_roster()
|
||||
|
||||
try:
|
||||
rtt = self['xep_0199'].ping(self.pingjid,
|
||||
timeout=10)
|
||||
rtt = yield from self['xep_0199'].ping(self.pingjid,
|
||||
timeout=10)
|
||||
logging.info("Success! RTT: %s", rtt)
|
||||
except IqError as e:
|
||||
logging.info("Error pinging %s: %s",
|
||||
@@ -78,8 +81,7 @@ if __name__ == '__main__':
|
||||
action="store_const", dest="loglevel",
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
parser.add_argument("-t", "--pingto", help="set jid to ping",
|
||||
action="store", type="string", dest="pingjid",
|
||||
default=None)
|
||||
dest="pingjid", default=None)
|
||||
|
||||
# JID and password options.
|
||||
parser.add_argument("-j", "--jid", dest="jid",
|
||||
|
||||
@@ -5,14 +5,16 @@ import logging
|
||||
from getpass import getpass
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import asyncio
|
||||
import slixmpp
|
||||
from slixmpp.exceptions import XMPPError
|
||||
from slixmpp.xmlstream import ET, tostring
|
||||
|
||||
|
||||
class PubsubClient(slixmpp.ClientXMPP):
|
||||
|
||||
def __init__(self, jid, password, server,
|
||||
node=None, action='list', data=''):
|
||||
node=None, action='nodes', data=''):
|
||||
super(PubsubClient, self).__init__(jid, password)
|
||||
|
||||
self.register_plugin('xep_0030')
|
||||
@@ -30,81 +32,83 @@ class PubsubClient(slixmpp.ClientXMPP):
|
||||
|
||||
self.add_event_handler('session_start', self.start)
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, event):
|
||||
self.get_roster()
|
||||
self.send_presence()
|
||||
|
||||
try:
|
||||
getattr(self, self.action)()
|
||||
yield from getattr(self, self.action)()
|
||||
except:
|
||||
logging.error('Could not execute: %s' % self.action)
|
||||
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)
|
||||
result = yield from 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.')
|
||||
logging.info(' - %s', str(item))
|
||||
except XMPPError as error:
|
||||
logging.error('Could not retrieve node list: %s', error.format())
|
||||
|
||||
def create(self):
|
||||
try:
|
||||
self['xep_0060'].create_node(self.pubsub_server, self.node)
|
||||
except:
|
||||
logging.error('Could not create node: %s' % self.node)
|
||||
yield from self['xep_0060'].create_node(self.pubsub_server, self.node)
|
||||
logging.info('Created node %s', self.node)
|
||||
except XMPPError as error:
|
||||
logging.error('Could not create node %s: %s', self.node, error.format())
|
||||
|
||||
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)
|
||||
yield from self['xep_0060'].delete_node(self.pubsub_server, self.node)
|
||||
logging.info('Deleted node %s', self.node)
|
||||
except XMPPError as error:
|
||||
logging.error('Could not delete node %s: %s', self.node, error.format())
|
||||
|
||||
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)
|
||||
result = yield from self['xep_0060'].publish(self.pubsub_server, self.node, payload=payload)
|
||||
logging.info('Published at item id: %s', result['pubsub']['publish']['item']['id'])
|
||||
except XMPPError as error:
|
||||
logging.error('Could not publish to %s: %s', self.node, error.format())
|
||||
|
||||
def get(self):
|
||||
try:
|
||||
result = self['xep_0060'].get_item(self.pubsub_server, self.node, self.data)
|
||||
result = yield from 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))
|
||||
logging.info('Retrieved item %s: %s', item['id'], tostring(item['payload']))
|
||||
except XMPPError as error:
|
||||
logging.error('Could not retrieve item %s from node %s: %s', self.data, self.node, error.format())
|
||||
|
||||
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))
|
||||
yield from self['xep_0060'].retract(self.pubsub_server, self.node, self.data)
|
||||
logging.info('Retracted item %s from node %s', self.data, self.node)
|
||||
except XMPPError as error:
|
||||
logging.error('Could not retract item %s from node %s: %s', self.data, self.node, error.format())
|
||||
|
||||
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)
|
||||
yield from self['xep_0060'].purge(self.pubsub_server, self.node)
|
||||
logging.info('Purged all items from node %s', self.node)
|
||||
except XMPPError as error:
|
||||
logging.error('Could not purge items from node %s: %s', self.node, error.format())
|
||||
|
||||
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))
|
||||
iq = yield from self['xep_0060'].subscribe(self.pubsub_server, self.node)
|
||||
subscription = iq['pubsub']['subscription']
|
||||
logging.info('Subscribed %s to node %s', subscription['jid'], subscription['node'])
|
||||
except XMPPError as error:
|
||||
logging.error('Could not subscribe %s to node %s: %s', self.boundjid.bare, self.node, error.format())
|
||||
|
||||
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))
|
||||
yield from self['xep_0060'].unsubscribe(self.pubsub_server, self.node)
|
||||
logging.info('Unsubscribed %s from node %s', self.boundjid.bare, self.node)
|
||||
except XMPPError as error:
|
||||
logging.error('Could not unsubscribe %s from node %s: %s', self.boundjid.bare, self.node, error.format())
|
||||
|
||||
|
||||
|
||||
@@ -121,12 +125,12 @@ if __name__ == '__main__':
|
||||
action="store_const",
|
||||
dest="loglevel",
|
||||
const=logging.ERROR,
|
||||
default=logging.ERROR)
|
||||
default=logging.INFO)
|
||||
parser.add_argument("-d","--debug", help="set logging to DEBUG",
|
||||
action="store_const",
|
||||
dest="loglevel",
|
||||
const=logging.DEBUG,
|
||||
default=logging.ERROR)
|
||||
default=logging.INFO)
|
||||
|
||||
# JID and password options.
|
||||
parser.add_argument("-j", "--jid", dest="jid",
|
||||
@@ -135,7 +139,7 @@ if __name__ == '__main__':
|
||||
help="password to use")
|
||||
|
||||
parser.add_argument("server")
|
||||
parser.add_argument("action", choice=["nodes", "create", "delete", "purge", "subscribe", "unsubscribe", "publish", "retract", "get"])
|
||||
parser.add_argument("action", choices=["nodes", "create", "delete", "purge", "subscribe", "unsubscribe", "publish", "retract", "get"])
|
||||
parser.add_argument("node", nargs='?')
|
||||
parser.add_argument("data", nargs='?')
|
||||
|
||||
@@ -159,4 +163,4 @@ if __name__ == '__main__':
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
xmpp.connect()
|
||||
xmpp.process()
|
||||
xmpp.process(forever=False)
|
||||
|
||||
@@ -90,7 +90,7 @@ class RegisterBot(slixmpp.ClientXMPP):
|
||||
resp['register']['password'] = self.password
|
||||
|
||||
try:
|
||||
resp.send()
|
||||
yield from resp.send()
|
||||
logging.info("Account created for %s!" % self.boundjid)
|
||||
except IqError as e:
|
||||
logging.error("Could not register account: %s" %
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
|
||||
import logging
|
||||
from getpass import getpass
|
||||
import threading
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import slixmpp
|
||||
from slixmpp.exceptions import IqError, IqTimeout
|
||||
from slixmpp.xmlstream.asyncio import asyncio
|
||||
|
||||
|
||||
class RosterBrowser(slixmpp.ClientXMPP):
|
||||
@@ -36,8 +36,9 @@ class RosterBrowser(slixmpp.ClientXMPP):
|
||||
self.add_event_handler("changed_status", self.wait_for_presences)
|
||||
|
||||
self.received = set()
|
||||
self.presences_received = threading.Event()
|
||||
self.presences_received = asyncio.Event()
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
@@ -51,17 +52,21 @@ class RosterBrowser(slixmpp.ClientXMPP):
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
future = asyncio.Future()
|
||||
def callback(result):
|
||||
future.set_result(None)
|
||||
try:
|
||||
self.get_roster()
|
||||
self.get_roster(callback=callback)
|
||||
yield from future
|
||||
except IqError as err:
|
||||
print('Error: %' % err.iq['error']['condition'])
|
||||
print('Error: %s' % err.iq['error']['condition'])
|
||||
except IqTimeout:
|
||||
print('Error: Request timed out')
|
||||
self.send_presence()
|
||||
|
||||
|
||||
print('Waiting for presence updates...\n')
|
||||
self.presences_received.wait(5)
|
||||
yield from asyncio.sleep(10)
|
||||
|
||||
print('Roster for %s' % self.boundjid.bare)
|
||||
groups = self.client_roster.groups()
|
||||
|
||||
@@ -20,7 +20,7 @@ class Boomerang(Endpoint):
|
||||
|
||||
@remote
|
||||
def throw(self):
|
||||
print "Duck!"
|
||||
print("Duck!")
|
||||
|
||||
|
||||
|
||||
|
||||
90
examples/s5b_transfer/s5b_receiver.py
Executable file
90
examples/s5b_transfer/s5b_receiver.py
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2015 Emmanuel Gil Peyrot
|
||||
This file is part of Slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from getpass import getpass
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import slixmpp
|
||||
|
||||
|
||||
class S5BReceiver(slixmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
A basic example of creating and using a SOCKS5 bytestream.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, filename):
|
||||
slixmpp.ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
self.file = open(filename, 'wb')
|
||||
|
||||
self.add_event_handler("socks5_connected", self.stream_opened)
|
||||
self.add_event_handler("socks5_data", self.stream_data)
|
||||
self.add_event_handler("socks5_closed", self.stream_closed)
|
||||
|
||||
def stream_opened(self, sid):
|
||||
logging.info('Stream opened. %s', sid)
|
||||
|
||||
def stream_data(self, data):
|
||||
self.file.write(data)
|
||||
|
||||
def stream_closed(self, exception):
|
||||
logging.info('Stream closed. %s', exception)
|
||||
self.file.close()
|
||||
self.disconnect()
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
parser = ArgumentParser()
|
||||
|
||||
# Output verbosity options.
|
||||
parser.add_argument("-q", "--quiet", help="set logging to ERROR",
|
||||
action="store_const", dest="loglevel",
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
parser.add_argument("-d", "--debug", help="set logging to DEBUG",
|
||||
action="store_const", dest="loglevel",
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
|
||||
# JID and password options.
|
||||
parser.add_argument("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
parser.add_argument("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
parser.add_argument("-o", "--out", dest="filename",
|
||||
help="file to save to")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=args.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if args.jid is None:
|
||||
args.jid = input("Username: ")
|
||||
if args.password is None:
|
||||
args.password = getpass("Password: ")
|
||||
if args.filename is None:
|
||||
args.filename = input("File path: ")
|
||||
|
||||
# Setup the S5BReceiver and register plugins. Note that while plugins may
|
||||
# have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = S5BReceiver(args.jid, args.password, args.filename)
|
||||
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||
xmpp.register_plugin('xep_0065', {
|
||||
'auto_accept': True
|
||||
}) # SOCKS5 Bytestreams
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
xmpp.connect()
|
||||
xmpp.process(forever=False)
|
||||
124
examples/s5b_transfer/s5b_sender.py
Executable file
124
examples/s5b_transfer/s5b_sender.py
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2015 Emmanuel Gil Peyrot
|
||||
This file is part of Slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from getpass import getpass
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import slixmpp
|
||||
from slixmpp.exceptions import IqError, IqTimeout
|
||||
|
||||
|
||||
class S5BSender(slixmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
A basic example of creating and using a SOCKS5 bytestream.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, receiver, filename):
|
||||
slixmpp.ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
self.receiver = receiver
|
||||
|
||||
self.file = open(filename, 'rb')
|
||||
|
||||
# The session_start event will be triggered when
|
||||
# the bot establishes its connection with the server
|
||||
# and the XML streams are ready for use.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
@asyncio.coroutine
|
||||
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.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Open the S5B stream in which to write to.
|
||||
proxy = yield from self['xep_0065'].handshake(self.receiver)
|
||||
|
||||
# Send the entire file.
|
||||
while True:
|
||||
data = self.file.read(1048576)
|
||||
if not data:
|
||||
break
|
||||
yield from proxy.write(data)
|
||||
|
||||
# And finally close the stream.
|
||||
proxy.transport.write_eof()
|
||||
except (IqError, IqTimeout):
|
||||
print('File transfer errored')
|
||||
else:
|
||||
print('File transfer finished')
|
||||
finally:
|
||||
self.file.close()
|
||||
self.disconnect()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
parser = ArgumentParser()
|
||||
|
||||
# Output verbosity options.
|
||||
parser.add_argument("-q", "--quiet", help="set logging to ERROR",
|
||||
action="store_const", dest="loglevel",
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
parser.add_argument("-d", "--debug", help="set logging to DEBUG",
|
||||
action="store_const", dest="loglevel",
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
|
||||
# JID and password options.
|
||||
parser.add_argument("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
parser.add_argument("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
parser.add_argument("-r", "--receiver", dest="receiver",
|
||||
help="JID of the receiver")
|
||||
parser.add_argument("-f", "--file", dest="filename",
|
||||
help="file to send")
|
||||
parser.add_argument("-m", "--use-messages", action="store_true",
|
||||
help="use messages instead of iqs for file transfer")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=args.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if args.jid is None:
|
||||
args.jid = input("Username: ")
|
||||
if args.password is None:
|
||||
args.password = getpass("Password: ")
|
||||
if args.receiver is None:
|
||||
args.receiver = input("Receiver: ")
|
||||
if args.filename is None:
|
||||
args.filename = input("File path: ")
|
||||
|
||||
# Setup the S5BSender and register plugins. Note that while plugins may
|
||||
# have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = S5BSender(args.jid, args.password, args.receiver, args.filename)
|
||||
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||
xmpp.register_plugin('xep_0065') # SOCKS5 Bytestreams
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
xmpp.connect()
|
||||
xmpp.process(forever=False)
|
||||
@@ -18,7 +18,7 @@ from argparse import ArgumentParser
|
||||
|
||||
import slixmpp
|
||||
from slixmpp.exceptions import XMPPError
|
||||
|
||||
from slixmpp import asyncio
|
||||
|
||||
class AvatarSetter(slixmpp.ClientXMPP):
|
||||
|
||||
@@ -33,6 +33,7 @@ class AvatarSetter(slixmpp.ClientXMPP):
|
||||
|
||||
self.filepath = filepath
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
@@ -51,7 +52,7 @@ class AvatarSetter(slixmpp.ClientXMPP):
|
||||
|
||||
avatar_file = None
|
||||
try:
|
||||
avatar_file = open(os.path.expanduser(self.filepath))
|
||||
avatar_file = open(os.path.expanduser(self.filepath), 'rb')
|
||||
except IOError:
|
||||
print('Could not find file: %s' % self.filepath)
|
||||
return self.disconnect()
|
||||
@@ -65,32 +66,31 @@ class AvatarSetter(slixmpp.ClientXMPP):
|
||||
avatar_file.close()
|
||||
|
||||
used_xep84 = False
|
||||
try:
|
||||
print('Publish XEP-0084 avatar data')
|
||||
self['xep_0084'].publish_avatar(avatar)
|
||||
used_xep84 = True
|
||||
except XMPPError:
|
||||
print('Could not publish XEP-0084 avatar')
|
||||
|
||||
try:
|
||||
print('Update vCard with avatar')
|
||||
self['xep_0153'].set_avatar(avatar=avatar, mtype=avatar_type)
|
||||
except XMPPError:
|
||||
print('Publish XEP-0084 avatar data')
|
||||
result = yield from self['xep_0084'].publish_avatar(avatar)
|
||||
if isinstance(result, XMPPError):
|
||||
print('Could not publish XEP-0084 avatar')
|
||||
else:
|
||||
used_xep84 = True
|
||||
|
||||
print('Update vCard with avatar')
|
||||
result = yield from self['xep_0153'].set_avatar(avatar=avatar, mtype=avatar_type)
|
||||
if isinstance(result, XMPPError):
|
||||
print('Could not set vCard avatar')
|
||||
|
||||
if used_xep84:
|
||||
try:
|
||||
print('Advertise XEP-0084 avatar metadata')
|
||||
self['xep_0084'].publish_avatar_metadata([
|
||||
{'id': avatar_id,
|
||||
'type': avatar_type,
|
||||
'bytes': avatar_bytes}
|
||||
# We could advertise multiple avatars to provide
|
||||
# options in image type, source (HTTP vs pubsub),
|
||||
# size, etc.
|
||||
# {'id': ....}
|
||||
])
|
||||
except XMPPError:
|
||||
print('Advertise XEP-0084 avatar metadata')
|
||||
result = yield from self['xep_0084'].publish_avatar_metadata([
|
||||
{'id': avatar_id,
|
||||
'type': avatar_type,
|
||||
'bytes': avatar_bytes}
|
||||
# We could advertise multiple avatars to provide
|
||||
# options in image type, source (HTTP vs pubsub),
|
||||
# size, etc.
|
||||
# {'id': ....}
|
||||
])
|
||||
if isinstance(result, XMPPError):
|
||||
print('Could not publish XEP-0084 metadata')
|
||||
|
||||
print('Wait for presence updates to propagate...')
|
||||
|
||||
11
setup.py
11
setup.py
@@ -13,6 +13,14 @@ try:
|
||||
except ImportError:
|
||||
from distutils.core import setup
|
||||
|
||||
try:
|
||||
from Cython.Build import cythonize
|
||||
except ImportError:
|
||||
print('Cython not found, falling back to the slow stringprep module.')
|
||||
ext_modules = None
|
||||
else:
|
||||
ext_modules = cythonize('slixmpp/stringprep.pyx')
|
||||
|
||||
from run_tests import TestCommand
|
||||
from slixmpp.version import __version__
|
||||
|
||||
@@ -43,7 +51,8 @@ setup(
|
||||
license='MIT',
|
||||
platforms=['any'],
|
||||
packages=packages,
|
||||
requires=['aiodns', 'pyasn1', 'pyasn1_modules'],
|
||||
ext_modules=ext_modules,
|
||||
install_requires=['aiodns>=1.0', 'pyasn1', 'pyasn1_modules'],
|
||||
classifiers=CLASSIFIERS,
|
||||
cmdclass={'test': TestCommand}
|
||||
)
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
if hasattr(asyncio, 'sslproto'): # no ssl proto: very old asyncio = no need for this
|
||||
asyncio.sslproto._is_sslproto_available=lambda: False
|
||||
import logging
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
|
||||
@@ -16,6 +19,7 @@ from slixmpp.xmlstream.stanzabase import ET, ElementBase, register_stanza_plugin
|
||||
from slixmpp.xmlstream.handler import *
|
||||
from slixmpp.xmlstream import XMLStream
|
||||
from slixmpp.xmlstream.matcher import *
|
||||
from slixmpp.xmlstream.asyncio import asyncio, future_wrapper
|
||||
from slixmpp.basexmpp import BaseXMPP
|
||||
from slixmpp.clientxmpp import ClientXMPP
|
||||
from slixmpp.componentxmpp import ComponentXMPP
|
||||
|
||||
@@ -22,7 +22,6 @@ from slixmpp.exceptions import IqError, IqTimeout
|
||||
from slixmpp.stanza import Message, Presence, Iq, StreamError
|
||||
from slixmpp.stanza.roster import Roster
|
||||
from slixmpp.stanza.nick import Nick
|
||||
from slixmpp.stanza.htmlim import HTMLIM
|
||||
|
||||
from slixmpp.xmlstream import XMLStream, JID
|
||||
from slixmpp.xmlstream import ET, register_stanza_plugin
|
||||
@@ -46,8 +45,8 @@ class BaseXMPP(XMLStream):
|
||||
is used during initialization.
|
||||
"""
|
||||
|
||||
def __init__(self, jid='', default_ns='jabber:client'):
|
||||
XMLStream.__init__(self)
|
||||
def __init__(self, jid='', default_ns='jabber:client', **kwargs):
|
||||
XMLStream.__init__(self, **kwargs)
|
||||
|
||||
self.default_ns = default_ns
|
||||
self.stream_ns = 'http://etherx.jabber.org/streams'
|
||||
@@ -57,12 +56,12 @@ class BaseXMPP(XMLStream):
|
||||
self.stream_id = None
|
||||
|
||||
#: The JabberID (JID) requested for this connection.
|
||||
self.requested_jid = JID(jid, cache_lock=True)
|
||||
self.requested_jid = JID(jid)
|
||||
|
||||
#: The JabberID (JID) used by this connection,
|
||||
#: as set after session binding. This may even be a
|
||||
#: different bare JID than what was requested.
|
||||
self.boundjid = JID(jid, cache_lock=True)
|
||||
self.boundjid = JID(jid)
|
||||
|
||||
self._expected_server_name = self.boundjid.host
|
||||
self._redirect_attempts = 0
|
||||
@@ -143,6 +142,13 @@ class BaseXMPP(XMLStream):
|
||||
MatchXPath('{%s}message/{%s}body' % (self.default_ns,
|
||||
self.default_ns)),
|
||||
self._handle_message))
|
||||
|
||||
self.register_handler(
|
||||
Callback('IMError',
|
||||
MatchXPath('{%s}message/{%s}error' % (self.default_ns,
|
||||
self.default_ns)),
|
||||
self._handle_message_error))
|
||||
|
||||
self.register_handler(
|
||||
Callback('Presence',
|
||||
MatchXPath("{%s}presence" % self.default_ns),
|
||||
@@ -203,9 +209,9 @@ class BaseXMPP(XMLStream):
|
||||
log.warning('Legacy XMPP 0.9 protocol detected.')
|
||||
self.event('legacy_protocol')
|
||||
|
||||
def process(self, timeout=None):
|
||||
def process(self, *, forever=True, timeout=None):
|
||||
self.init_plugins()
|
||||
XMLStream.process(self, timeout)
|
||||
XMLStream.process(self, forever=forever, timeout=timeout)
|
||||
|
||||
def init_plugins(self):
|
||||
for name in self.plugin:
|
||||
@@ -214,7 +220,7 @@ class BaseXMPP(XMLStream):
|
||||
self.plugin[name].post_init()
|
||||
self.plugin[name].post_inited = True
|
||||
|
||||
def register_plugin(self, plugin, pconfig={}, module=None):
|
||||
def register_plugin(self, plugin, pconfig=None, module=None):
|
||||
"""Register and configure a plugin for use in this stream.
|
||||
|
||||
:param plugin: The name of the plugin class. Plugin names must
|
||||
@@ -631,7 +637,7 @@ class BaseXMPP(XMLStream):
|
||||
def set_jid(self, jid):
|
||||
"""Rip a JID apart and claim it as our own."""
|
||||
log.debug("setting jid to %s", jid)
|
||||
self.boundjid = JID(jid, cache_lock=True)
|
||||
self.boundjid = JID(jid)
|
||||
|
||||
def getjidresource(self, fulljid):
|
||||
if '/' in fulljid:
|
||||
@@ -690,6 +696,12 @@ class BaseXMPP(XMLStream):
|
||||
msg['to'] = self.boundjid
|
||||
self.event('message', msg)
|
||||
|
||||
def _handle_message_error(self, msg):
|
||||
"""Process incoming message error stanzas."""
|
||||
if not self.is_component and not msg['to'].bare:
|
||||
msg['to'] = self.boundjid
|
||||
self.event('message_error', msg)
|
||||
|
||||
def _handle_available(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_available(pres)
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ class ClientXMPP(BaseXMPP):
|
||||
|
||||
:param jid: The JID of the XMPP user account.
|
||||
:param password: The password for the XMPP user account.
|
||||
:param ssl: **Deprecated.**
|
||||
:param plugin_config: A dictionary of plugin configurations.
|
||||
:param plugin_whitelist: A list of approved plugins that
|
||||
will be loaded when calling
|
||||
@@ -58,9 +57,15 @@ class ClientXMPP(BaseXMPP):
|
||||
:param escape_quotes: **Deprecated.**
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, plugin_config={}, plugin_whitelist=[],
|
||||
escape_quotes=True, sasl_mech=None, lang='en'):
|
||||
BaseXMPP.__init__(self, jid, 'jabber:client')
|
||||
def __init__(self, jid, password, plugin_config=None,
|
||||
plugin_whitelist=None, escape_quotes=True, sasl_mech=None,
|
||||
lang='en', **kwargs):
|
||||
if not plugin_whitelist:
|
||||
plugin_whitelist = []
|
||||
if not plugin_config:
|
||||
plugin_config = {}
|
||||
|
||||
BaseXMPP.__init__(self, jid, 'jabber:client', **kwargs)
|
||||
|
||||
self.escape_quotes = escape_quotes
|
||||
self.plugin_config = plugin_config
|
||||
@@ -135,8 +140,6 @@ class ClientXMPP(BaseXMPP):
|
||||
will be used.
|
||||
|
||||
:param address: A tuple containing the server's host and port.
|
||||
:param reattempt: If ``True``, repeat attempting to connect if an
|
||||
error occurs. Defaults to ``True``.
|
||||
:param use_tls: Indicates if TLS should be used for the
|
||||
connection. Defaults to ``True``.
|
||||
:param use_ssl: Indicates if the older SSL connection method
|
||||
|
||||
@@ -46,8 +46,13 @@ class ComponentXMPP(BaseXMPP):
|
||||
Defaults to ``False``.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, secret, host=None, port=None,
|
||||
plugin_config={}, plugin_whitelist=[], use_jc_ns=False):
|
||||
def __init__(self, jid, secret, host=None, port=None, plugin_config=None, plugin_whitelist=None, use_jc_ns=False):
|
||||
|
||||
if not plugin_whitelist:
|
||||
plugin_whitelist = []
|
||||
if not plugin_config:
|
||||
plugin_config = {}
|
||||
|
||||
if use_jc_ns:
|
||||
default_ns = 'jabber:client'
|
||||
else:
|
||||
@@ -55,7 +60,7 @@ class ComponentXMPP(BaseXMPP):
|
||||
BaseXMPP.__init__(self, jid, default_ns)
|
||||
|
||||
self.auto_authorize = None
|
||||
self.stream_header = "<stream:stream %s %s to='%s'>" % (
|
||||
self.stream_header = '<stream:stream %s %s to="%s">' % (
|
||||
'xmlns="jabber:component:accept"',
|
||||
'xmlns:stream="%s"' % self.stream_ns,
|
||||
jid)
|
||||
@@ -68,6 +73,8 @@ class ComponentXMPP(BaseXMPP):
|
||||
self.plugin_whitelist = plugin_whitelist
|
||||
self.is_component = True
|
||||
|
||||
self.sessionstarted = False
|
||||
|
||||
self.register_handler(
|
||||
Callback('Handshake',
|
||||
MatchXPath('{jabber:component:accept}handshake'),
|
||||
@@ -75,12 +82,9 @@ class ComponentXMPP(BaseXMPP):
|
||||
self.add_event_handler('presence_probe',
|
||||
self._handle_probe)
|
||||
|
||||
def connect(self, host=None, port=None, use_ssl=False,
|
||||
use_tls=False, reattempt=True):
|
||||
def connect(self, host=None, port=None, use_ssl=False):
|
||||
"""Connect to the server.
|
||||
|
||||
Setting ``reattempt`` to ``True`` will cause connection attempts to
|
||||
be made every second until a successful connection is established.
|
||||
|
||||
:param host: The name of the desired server for the connection.
|
||||
Defaults to :attr:`server_host`.
|
||||
@@ -88,11 +92,6 @@ class ComponentXMPP(BaseXMPP):
|
||||
Defauts to :attr:`server_port`.
|
||||
:param use_ssl: Flag indicating if SSL should be used by connecting
|
||||
directly to a port using SSL.
|
||||
:param use_tls: Flag indicating if TLS should be used, allowing for
|
||||
connecting to a port without using SSL immediately and
|
||||
later upgrading the connection.
|
||||
:param reattempt: Flag indicating if the socket should reconnect
|
||||
after disconnections.
|
||||
"""
|
||||
if host is None:
|
||||
host = self.server_host
|
||||
@@ -101,14 +100,9 @@ class ComponentXMPP(BaseXMPP):
|
||||
|
||||
self.server_name = self.boundjid.host
|
||||
|
||||
if use_tls:
|
||||
log.info("XEP-0114 components can not use TLS")
|
||||
|
||||
log.debug("Connecting to %s:%s", host, port)
|
||||
return XMLStream.connect(self, host=host, port=port,
|
||||
use_ssl=use_ssl,
|
||||
use_tls=False,
|
||||
reattempt=reattempt)
|
||||
use_ssl=use_ssl)
|
||||
|
||||
def incoming_filter(self, xml):
|
||||
"""
|
||||
@@ -145,7 +139,7 @@ class ComponentXMPP(BaseXMPP):
|
||||
:param xml: The reply handshake stanza.
|
||||
"""
|
||||
self.session_bind_event.set()
|
||||
self.session_started_event.set()
|
||||
self.sessionstarted = True
|
||||
self.event('session_bind', self.boundjid)
|
||||
self.event('session_start')
|
||||
|
||||
|
||||
@@ -56,6 +56,18 @@ class XMPPError(Exception):
|
||||
self.extension_ns = extension_ns
|
||||
self.extension_args = extension_args
|
||||
|
||||
def format(self):
|
||||
"""
|
||||
Format the error in a simple user-readable string.
|
||||
"""
|
||||
text = [self.etype, self.condition]
|
||||
if self.text:
|
||||
text.append(self.text)
|
||||
if self.extension:
|
||||
text.append(self.extension)
|
||||
# TODO: handle self.extension_args
|
||||
return ': '.join(text)
|
||||
|
||||
|
||||
class IqTimeout(XMPPError):
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class FeatureBind(BasePlugin):
|
||||
iq.send(callback=self._on_bind_response)
|
||||
|
||||
def _on_bind_response(self, response):
|
||||
self.xmpp.boundjid = JID(response['bind']['jid'], cache_lock=True)
|
||||
self.xmpp.boundjid = JID(response['bind']['jid'])
|
||||
self.xmpp.bound = True
|
||||
self.xmpp.event('session_bind', self.xmpp.boundjid)
|
||||
self.xmpp.session_bind_event.set()
|
||||
|
||||
@@ -190,14 +190,14 @@ class FeatureMechanisms(BasePlugin):
|
||||
except sasl.SASLCancelled:
|
||||
self.attempted_mechs.add(self.mech.name)
|
||||
self._send_auth()
|
||||
except sasl.SASLFailed:
|
||||
self.attempted_mechs.add(self.mech.name)
|
||||
self._send_auth()
|
||||
except sasl.SASLMutualAuthFailed:
|
||||
log.error("Mutual authentication failed! " + \
|
||||
"A security breach is possible.")
|
||||
self.attempted_mechs.add(self.mech.name)
|
||||
self.xmpp.disconnect()
|
||||
except sasl.SASLFailed:
|
||||
self.attempted_mechs.add(self.mech.name)
|
||||
self._send_auth()
|
||||
else:
|
||||
resp.send()
|
||||
|
||||
@@ -210,13 +210,13 @@ class FeatureMechanisms(BasePlugin):
|
||||
resp['value'] = self.mech.process(stanza['value'])
|
||||
except sasl.SASLCancelled:
|
||||
self.stanza.Abort(self.xmpp).send()
|
||||
except sasl.SASLFailed:
|
||||
self.stanza.Abort(self.xmpp).send()
|
||||
except sasl.SASLMutualAuthFailed:
|
||||
log.error("Mutual authentication failed! " + \
|
||||
"A security breach is possible.")
|
||||
self.attempted_mechs.add(self.mech.name)
|
||||
self.xmpp.disconnect()
|
||||
except sasl.SASLFailed:
|
||||
self.stanza.Abort(self.xmpp).send()
|
||||
else:
|
||||
if resp.get_value() == '':
|
||||
resp.del_value()
|
||||
|
||||
495
slixmpp/jid.py
495
slixmpp/jid.py
@@ -11,24 +11,15 @@
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import socket
|
||||
import stringprep
|
||||
import threading
|
||||
import encodings.idna
|
||||
|
||||
from copy import deepcopy
|
||||
from functools import lru_cache
|
||||
|
||||
from slixmpp.util import stringprep_profiles
|
||||
from collections import OrderedDict
|
||||
from slixmpp.stringprep import nodeprep, resourceprep, idna, StringprepError
|
||||
|
||||
#: These characters are not allowed to appear in a JID.
|
||||
ILLEGAL_CHARS = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r' + \
|
||||
'\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19' + \
|
||||
'\x1a\x1b\x1c\x1d\x1e\x1f' + \
|
||||
' !"#$%&\'()*+,./:;<=>?@[\\]^_`{|}~\x7f'
|
||||
HAVE_INET_PTON = hasattr(socket, 'inet_pton')
|
||||
|
||||
#: The basic regex pattern that a JID must match in order to determine
|
||||
#: the local, domain, and resource parts. This regex does NOT do any
|
||||
@@ -38,22 +29,8 @@ JID_PATTERN = re.compile(
|
||||
)
|
||||
|
||||
#: The set of escape sequences for the characters not allowed by nodeprep.
|
||||
JID_ESCAPE_SEQUENCES = set(['\\20', '\\22', '\\26', '\\27', '\\2f',
|
||||
'\\3a', '\\3c', '\\3e', '\\40', '\\5c'])
|
||||
|
||||
#: A mapping of unallowed characters to their escape sequences. An escape
|
||||
#: sequence for '\' is also included since it must also be escaped in
|
||||
#: certain situations.
|
||||
JID_ESCAPE_TRANSFORMATIONS = {' ': '\\20',
|
||||
'"': '\\22',
|
||||
'&': '\\26',
|
||||
"'": '\\27',
|
||||
'/': '\\2f',
|
||||
':': '\\3a',
|
||||
'<': '\\3c',
|
||||
'>': '\\3e',
|
||||
'@': '\\40',
|
||||
'\\': '\\5c'}
|
||||
JID_ESCAPE_SEQUENCES = {'\\20', '\\22', '\\26', '\\27', '\\2f',
|
||||
'\\3a', '\\3c', '\\3e', '\\40', '\\5c'}
|
||||
|
||||
#: The reverse mapping of escape sequences to their original forms.
|
||||
JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
|
||||
@@ -67,70 +44,9 @@ JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
|
||||
'\\40': '@',
|
||||
'\\5c': '\\'}
|
||||
|
||||
JID_CACHE = OrderedDict()
|
||||
JID_CACHE_LOCK = threading.Lock()
|
||||
JID_CACHE_MAX_SIZE = 1024
|
||||
|
||||
def _cache(key, parts, locked):
|
||||
JID_CACHE[key] = (parts, locked)
|
||||
if len(JID_CACHE) > JID_CACHE_MAX_SIZE:
|
||||
with JID_CACHE_LOCK:
|
||||
while len(JID_CACHE) > JID_CACHE_MAX_SIZE:
|
||||
found = None
|
||||
for key, item in JID_CACHE.items():
|
||||
if not item[1]: # if not locked
|
||||
found = key
|
||||
break
|
||||
if not found: # more than MAX_SIZE locked
|
||||
# warn?
|
||||
break
|
||||
del JID_CACHE[found]
|
||||
|
||||
# pylint: disable=c0103
|
||||
#: The nodeprep profile of stringprep used to validate the local,
|
||||
#: or username, portion of a JID.
|
||||
nodeprep = stringprep_profiles.create(
|
||||
nfkc=True,
|
||||
bidi=True,
|
||||
mappings=[
|
||||
stringprep_profiles.b1_mapping,
|
||||
stringprep.map_table_b2],
|
||||
prohibited=[
|
||||
stringprep.in_table_c11,
|
||||
stringprep.in_table_c12,
|
||||
stringprep.in_table_c21,
|
||||
stringprep.in_table_c22,
|
||||
stringprep.in_table_c3,
|
||||
stringprep.in_table_c4,
|
||||
stringprep.in_table_c5,
|
||||
stringprep.in_table_c6,
|
||||
stringprep.in_table_c7,
|
||||
stringprep.in_table_c8,
|
||||
stringprep.in_table_c9,
|
||||
lambda c: c in ' \'"&/:<>@'],
|
||||
unassigned=[stringprep.in_table_a1])
|
||||
|
||||
# pylint: disable=c0103
|
||||
#: The resourceprep profile of stringprep, which is used to validate
|
||||
#: the resource portion of a JID.
|
||||
resourceprep = stringprep_profiles.create(
|
||||
nfkc=True,
|
||||
bidi=True,
|
||||
mappings=[stringprep_profiles.b1_mapping],
|
||||
prohibited=[
|
||||
stringprep.in_table_c12,
|
||||
stringprep.in_table_c21,
|
||||
stringprep.in_table_c22,
|
||||
stringprep.in_table_c3,
|
||||
stringprep.in_table_c4,
|
||||
stringprep.in_table_c5,
|
||||
stringprep.in_table_c6,
|
||||
stringprep.in_table_c7,
|
||||
stringprep.in_table_c8,
|
||||
stringprep.in_table_c9],
|
||||
unassigned=[stringprep.in_table_a1])
|
||||
|
||||
|
||||
# TODO: Find the best cache size for a standard usage.
|
||||
@lru_cache(maxsize=1024)
|
||||
def _parse_jid(data):
|
||||
"""
|
||||
Parse string data into the node, domain, and resource
|
||||
@@ -162,17 +78,19 @@ def _validate_node(node):
|
||||
|
||||
:returns: The local portion of a JID, as validated by nodeprep.
|
||||
"""
|
||||
try:
|
||||
if node is not None:
|
||||
node = nodeprep(node)
|
||||
if node is None:
|
||||
return None
|
||||
|
||||
if not node:
|
||||
raise InvalidJID('Localpart must not be 0 bytes')
|
||||
if len(node) > 1023:
|
||||
raise InvalidJID('Localpart must be less than 1024 bytes')
|
||||
return node
|
||||
except stringprep_profiles.StringPrepError:
|
||||
raise InvalidJID('Invalid local part')
|
||||
try:
|
||||
node = nodeprep(node)
|
||||
except StringprepError:
|
||||
raise InvalidJID('Nodeprep failed')
|
||||
|
||||
if not node:
|
||||
raise InvalidJID('Localpart must not be 0 bytes')
|
||||
if len(node) > 1023:
|
||||
raise InvalidJID('Localpart must be less than 1024 bytes')
|
||||
return node
|
||||
|
||||
|
||||
def _validate_domain(domain):
|
||||
@@ -199,10 +117,10 @@ def _validate_domain(domain):
|
||||
pass
|
||||
|
||||
# Check if this is an IPv6 address
|
||||
if not ip_addr and hasattr(socket, 'inet_pton'):
|
||||
if not ip_addr and HAVE_INET_PTON and domain[0] == '[' and domain[-1] == ']':
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, domain.strip('[]'))
|
||||
domain = '[%s]' % domain.strip('[]')
|
||||
ip = domain[1:-1]
|
||||
socket.inet_pton(socket.AF_INET6, ip)
|
||||
ip_addr = True
|
||||
except (socket.error, ValueError):
|
||||
pass
|
||||
@@ -213,31 +131,19 @@ def _validate_domain(domain):
|
||||
if domain and domain[-1] == '.':
|
||||
domain = domain[:-1]
|
||||
|
||||
domain_parts = []
|
||||
try:
|
||||
domain = idna(domain)
|
||||
except StringprepError:
|
||||
raise InvalidJID('idna validation failed')
|
||||
|
||||
if ':' in domain:
|
||||
raise InvalidJID('Domain containing a port')
|
||||
for label in domain.split('.'):
|
||||
try:
|
||||
label = encodings.idna.nameprep(label)
|
||||
encodings.idna.ToASCII(label)
|
||||
pass_nameprep = True
|
||||
except UnicodeError:
|
||||
pass_nameprep = False
|
||||
|
||||
if not pass_nameprep:
|
||||
raise InvalidJID('Could not encode domain as ASCII')
|
||||
|
||||
if label.startswith('xn--'):
|
||||
label = encodings.idna.ToUnicode(label)
|
||||
|
||||
for char in label:
|
||||
if char in ILLEGAL_CHARS:
|
||||
raise InvalidJID('Domain contains illegal characters')
|
||||
|
||||
if not label:
|
||||
raise InvalidJID('Domain containing too many dots')
|
||||
if '-' in (label[0], label[-1]):
|
||||
raise InvalidJID('Domain started or ended with -')
|
||||
|
||||
domain_parts.append(label)
|
||||
domain = '.'.join(domain_parts)
|
||||
|
||||
if not domain:
|
||||
raise InvalidJID('Domain must not be 0 bytes')
|
||||
if len(domain) > 1023:
|
||||
@@ -253,42 +159,19 @@ def _validate_resource(resource):
|
||||
|
||||
:returns: The local portion of a JID, as validated by resourceprep.
|
||||
"""
|
||||
if resource is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
if resource is not None:
|
||||
resource = resourceprep(resource)
|
||||
resource = resourceprep(resource)
|
||||
except StringprepError:
|
||||
raise InvalidJID('Resourceprep failed')
|
||||
|
||||
if not resource:
|
||||
raise InvalidJID('Resource must not be 0 bytes')
|
||||
if len(resource) > 1023:
|
||||
raise InvalidJID('Resource must be less than 1024 bytes')
|
||||
return resource
|
||||
except stringprep_profiles.StringPrepError:
|
||||
raise InvalidJID('Invalid resource')
|
||||
|
||||
|
||||
def _escape_node(node):
|
||||
"""Escape the local portion of a JID."""
|
||||
result = []
|
||||
|
||||
for i, char in enumerate(node):
|
||||
if char == '\\':
|
||||
if ''.join((node[i:i+3])) in JID_ESCAPE_SEQUENCES:
|
||||
result.append('\\5c')
|
||||
continue
|
||||
result.append(char)
|
||||
|
||||
for i, char in enumerate(result):
|
||||
if char != '\\':
|
||||
result[i] = JID_ESCAPE_TRANSFORMATIONS.get(char, char)
|
||||
|
||||
escaped = ''.join(result)
|
||||
|
||||
if escaped.startswith('\\20') or escaped.endswith('\\20'):
|
||||
raise InvalidJID('Escaped local part starts or ends with "\\20"')
|
||||
|
||||
_validate_node(escaped)
|
||||
|
||||
return escaped
|
||||
if not resource:
|
||||
raise InvalidJID('Resource must not be 0 bytes')
|
||||
if len(resource) > 1023:
|
||||
raise InvalidJID('Resource must be less than 1024 bytes')
|
||||
return resource
|
||||
|
||||
|
||||
def _unescape_node(node):
|
||||
@@ -313,9 +196,7 @@ def _unescape_node(node):
|
||||
seq = seq[1:]
|
||||
else:
|
||||
unescaped.append(char)
|
||||
unescaped = ''.join(unescaped)
|
||||
|
||||
return unescaped
|
||||
return ''.join(unescaped)
|
||||
|
||||
|
||||
def _format_jid(local=None, domain=None, resource=None):
|
||||
@@ -328,12 +209,12 @@ def _format_jid(local=None, domain=None, resource=None):
|
||||
:return: A full or bare JID string.
|
||||
"""
|
||||
result = []
|
||||
if local:
|
||||
if local is not None:
|
||||
result.append(local)
|
||||
result.append('@')
|
||||
if domain:
|
||||
if domain is not None:
|
||||
result.append(domain)
|
||||
if resource:
|
||||
if resource is not None:
|
||||
result.append('/')
|
||||
result.append(resource)
|
||||
return ''.join(result)
|
||||
@@ -349,47 +230,47 @@ class InvalidJID(ValueError):
|
||||
"""
|
||||
|
||||
# pylint: disable=R0903
|
||||
class UnescapedJID(object):
|
||||
class UnescapedJID:
|
||||
|
||||
"""
|
||||
.. versionadded:: 1.1.10
|
||||
"""
|
||||
|
||||
def __init__(self, local, domain, resource):
|
||||
self._jid = (local, domain, resource)
|
||||
__slots__ = ('_node', '_domain', '_resource')
|
||||
|
||||
# pylint: disable=R0911
|
||||
def __getattr__(self, name):
|
||||
def __init__(self, node, domain, resource):
|
||||
self._node = node
|
||||
self._domain = domain
|
||||
self._resource = resource
|
||||
|
||||
def __getattribute__(self, name):
|
||||
"""Retrieve the given JID component.
|
||||
|
||||
:param name: one of: user, server, domain, resource,
|
||||
full, or bare.
|
||||
"""
|
||||
if name == 'resource':
|
||||
return self._jid[2] or ''
|
||||
elif name in ('user', 'username', 'local', 'node'):
|
||||
return self._jid[0] or ''
|
||||
elif name in ('server', 'domain', 'host'):
|
||||
return self._jid[1] or ''
|
||||
elif name in ('full', 'jid'):
|
||||
return _format_jid(*self._jid)
|
||||
elif name == 'bare':
|
||||
return _format_jid(self._jid[0], self._jid[1])
|
||||
elif name == '_jid':
|
||||
return getattr(super(JID, self), '_jid')
|
||||
else:
|
||||
return None
|
||||
return self._resource or ''
|
||||
if name in ('user', 'username', 'local', 'node'):
|
||||
return self._node or ''
|
||||
if name in ('server', 'domain', 'host'):
|
||||
return self._domain or ''
|
||||
if name in ('full', 'jid'):
|
||||
return _format_jid(self._node, self._domain, self._resource)
|
||||
if name == 'bare':
|
||||
return _format_jid(self._node, self._domain)
|
||||
return object.__getattribute__(self, name)
|
||||
|
||||
def __str__(self):
|
||||
"""Use the full JID as the string value."""
|
||||
return _format_jid(*self._jid)
|
||||
return _format_jid(self._node, self._domain, self._resource)
|
||||
|
||||
def __repr__(self):
|
||||
"""Use the full JID as the representation."""
|
||||
return self.__str__()
|
||||
return _format_jid(self._node, self._domain, self._resource)
|
||||
|
||||
|
||||
class JID(object):
|
||||
class JID:
|
||||
|
||||
"""
|
||||
A representation of a Jabber ID, or JID.
|
||||
@@ -401,13 +282,13 @@ class JID(object):
|
||||
The JID is a full JID otherwise.
|
||||
|
||||
**JID Properties:**
|
||||
:jid: Alias for ``full``.
|
||||
:full: The string value of the full JID.
|
||||
:jid: Alias for ``full``.
|
||||
:bare: The string value of the bare JID.
|
||||
:user: The username portion of the JID.
|
||||
:username: Alias for ``user``.
|
||||
:local: Alias for ``user``.
|
||||
:node: Alias for ``user``.
|
||||
:node: The node portion of the JID.
|
||||
:user: Alias for ``node``.
|
||||
:local: Alias for ``node``.
|
||||
:username: Alias for ``node``.
|
||||
:domain: The domain name portion of the JID.
|
||||
:server: Alias for ``domain``.
|
||||
:host: Alias for ``domain``.
|
||||
@@ -415,67 +296,23 @@ class JID(object):
|
||||
|
||||
:param string jid:
|
||||
A string of the form ``'[user@]domain[/resource]'``.
|
||||
:param string local:
|
||||
Optional. Specify the local, or username, portion
|
||||
of the JID. If provided, it will override the local
|
||||
value provided by the `jid` parameter. The given
|
||||
local value will also be escaped if necessary.
|
||||
:param string domain:
|
||||
Optional. Specify the domain of the JID. If
|
||||
provided, it will override the domain given by
|
||||
the `jid` parameter.
|
||||
:param string resource:
|
||||
Optional. Specify the resource value of the JID.
|
||||
If provided, it will override the domain given
|
||||
by the `jid` parameter.
|
||||
|
||||
:raises InvalidJID:
|
||||
"""
|
||||
|
||||
# pylint: disable=W0212
|
||||
def __init__(self, jid=None, **kwargs):
|
||||
locked = kwargs.get('cache_lock', False)
|
||||
in_local = kwargs.get('local', None)
|
||||
in_domain = kwargs.get('domain', None)
|
||||
in_resource = kwargs.get('resource', None)
|
||||
parts = None
|
||||
if in_local or in_domain or in_resource:
|
||||
parts = (in_local, in_domain, in_resource)
|
||||
__slots__ = ('_node', '_domain', '_resource')
|
||||
|
||||
# only check cache if there is a jid string, or parts, not if there
|
||||
# are both
|
||||
self._jid = None
|
||||
key = None
|
||||
if (jid is not None) and (parts is None):
|
||||
if isinstance(jid, JID):
|
||||
# it's already good to go, and there are no additions
|
||||
self._jid = jid._jid
|
||||
return
|
||||
key = jid
|
||||
self._jid, locked = JID_CACHE.get(jid, (None, locked))
|
||||
elif jid is None and parts is not None:
|
||||
key = parts
|
||||
self._jid, locked = JID_CACHE.get(parts, (None, locked))
|
||||
if not self._jid:
|
||||
if not jid:
|
||||
parsed_jid = (None, None, None)
|
||||
elif not isinstance(jid, JID):
|
||||
parsed_jid = _parse_jid(jid)
|
||||
else:
|
||||
parsed_jid = jid._jid
|
||||
|
||||
local, domain, resource = parsed_jid
|
||||
|
||||
if 'local' in kwargs:
|
||||
local = _escape_node(in_local)
|
||||
if 'domain' in kwargs:
|
||||
domain = _validate_domain(in_domain)
|
||||
if 'resource' in kwargs:
|
||||
resource = _validate_resource(in_resource)
|
||||
|
||||
self._jid = (local, domain, resource)
|
||||
if key:
|
||||
_cache(key, self._jid, locked)
|
||||
def __init__(self, jid=None):
|
||||
if not jid:
|
||||
self._node = None
|
||||
self._domain = None
|
||||
self._resource = None
|
||||
elif not isinstance(jid, JID):
|
||||
self._node, self._domain, self._resource = _parse_jid(jid)
|
||||
else:
|
||||
self._node = jid._node
|
||||
self._domain = jid._domain
|
||||
self._resource = jid._resource
|
||||
|
||||
def unescape(self):
|
||||
"""Return an unescaped JID object.
|
||||
@@ -488,151 +325,125 @@ class JID(object):
|
||||
|
||||
.. versionadded:: 1.1.10
|
||||
"""
|
||||
return UnescapedJID(_unescape_node(self._jid[0]),
|
||||
self._jid[1],
|
||||
self._jid[2])
|
||||
|
||||
def regenerate(self):
|
||||
"""No-op
|
||||
|
||||
.. deprecated:: 1.1.10
|
||||
"""
|
||||
pass
|
||||
|
||||
def reset(self, data):
|
||||
"""Start fresh from a new JID string.
|
||||
|
||||
:param string data: A string of the form ``'[user@]domain[/resource]'``.
|
||||
|
||||
.. deprecated:: 1.1.10
|
||||
"""
|
||||
self._jid = JID(data)._jid
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
return self._jid[2] or ''
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self._jid[0] or ''
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
return self._jid[0] or ''
|
||||
return UnescapedJID(_unescape_node(self._node),
|
||||
self._domain,
|
||||
self._resource)
|
||||
|
||||
@property
|
||||
def node(self):
|
||||
return self._jid[0] or ''
|
||||
return self._node or ''
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self._node or ''
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
return self._node or ''
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
return self._jid[0] or ''
|
||||
|
||||
@property
|
||||
def bare(self):
|
||||
return _format_jid(self._jid[0], self._jid[1])
|
||||
|
||||
@property
|
||||
def server(self):
|
||||
return self._jid[1] or ''
|
||||
return self._node or ''
|
||||
|
||||
@property
|
||||
def domain(self):
|
||||
return self._jid[1] or ''
|
||||
return self._domain or ''
|
||||
|
||||
@property
|
||||
def server(self):
|
||||
return self._domain or ''
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self._jid[1] or ''
|
||||
return self._domain or ''
|
||||
|
||||
@property
|
||||
def full(self):
|
||||
return _format_jid(*self._jid)
|
||||
|
||||
@property
|
||||
def jid(self):
|
||||
return _format_jid(*self._jid)
|
||||
def resource(self):
|
||||
return self._resource or ''
|
||||
|
||||
@property
|
||||
def bare(self):
|
||||
return _format_jid(self._jid[0], self._jid[1])
|
||||
return _format_jid(self._node, self._domain)
|
||||
|
||||
@property
|
||||
def full(self):
|
||||
return _format_jid(self._node, self._domain, self._resource)
|
||||
|
||||
@resource.setter
|
||||
def resource(self, value):
|
||||
self._jid = JID(self, resource=value)._jid
|
||||
|
||||
@user.setter
|
||||
def user(self, value):
|
||||
self._jid = JID(self, local=value)._jid
|
||||
|
||||
@username.setter
|
||||
def username(self, value):
|
||||
self._jid = JID(self, local=value)._jid
|
||||
|
||||
@local.setter
|
||||
def local(self, value):
|
||||
self._jid = JID(self, local=value)._jid
|
||||
@property
|
||||
def jid(self):
|
||||
return _format_jid(self._node, self._domain, self._resource)
|
||||
|
||||
@node.setter
|
||||
def node(self, value):
|
||||
self._jid = JID(self, local=value)._jid
|
||||
self._node = _validate_node(value)
|
||||
|
||||
@server.setter
|
||||
def server(self, value):
|
||||
self._jid = JID(self, domain=value)._jid
|
||||
@user.setter
|
||||
def user(self, value):
|
||||
self._node = _validate_node(value)
|
||||
|
||||
@local.setter
|
||||
def local(self, value):
|
||||
self._node = _validate_node(value)
|
||||
|
||||
@username.setter
|
||||
def username(self, value):
|
||||
self._node = _validate_node(value)
|
||||
|
||||
@domain.setter
|
||||
def domain(self, value):
|
||||
self._jid = JID(self, domain=value)._jid
|
||||
self._domain = _validate_domain(value)
|
||||
|
||||
@server.setter
|
||||
def server(self, value):
|
||||
self._domain = _validate_domain(value)
|
||||
|
||||
@host.setter
|
||||
def host(self, value):
|
||||
self._jid = JID(self, domain=value)._jid
|
||||
|
||||
@full.setter
|
||||
def full(self, value):
|
||||
self._jid = JID(value)._jid
|
||||
|
||||
@jid.setter
|
||||
def jid(self, value):
|
||||
self._jid = JID(value)._jid
|
||||
self._domain = _validate_domain(value)
|
||||
|
||||
@bare.setter
|
||||
def bare(self, value):
|
||||
parsed = JID(value)._jid
|
||||
self._jid = (parsed[0], parsed[1], self._jid[2])
|
||||
node, domain, resource = _parse_jid(value)
|
||||
assert not resource
|
||||
self._node = node
|
||||
self._domain = domain
|
||||
|
||||
@resource.setter
|
||||
def resource(self, value):
|
||||
self._resource = _validate_resource(value)
|
||||
|
||||
@full.setter
|
||||
def full(self, value):
|
||||
self._node, self._domain, self._resource = _parse_jid(value)
|
||||
|
||||
@jid.setter
|
||||
def jid(self, value):
|
||||
self._node, self._domain, self._resource = _parse_jid(value)
|
||||
|
||||
def __str__(self):
|
||||
"""Use the full JID as the string value."""
|
||||
return _format_jid(*self._jid)
|
||||
return _format_jid(self._node, self._domain, self._resource)
|
||||
|
||||
def __repr__(self):
|
||||
"""Use the full JID as the representation."""
|
||||
return self.__str__()
|
||||
return _format_jid(self._node, self._domain, self._resource)
|
||||
|
||||
# pylint: disable=W0212
|
||||
def __eq__(self, other):
|
||||
"""Two JIDs are equal if they have the same full JID value."""
|
||||
if isinstance(other, UnescapedJID):
|
||||
return False
|
||||
if not isinstance(other, JID):
|
||||
other = JID(other)
|
||||
|
||||
other = JID(other)
|
||||
return self._jid == other._jid
|
||||
return (self._node == other._node and
|
||||
self._domain == other._domain and
|
||||
self._resource == other._resource)
|
||||
|
||||
# pylint: disable=W0212
|
||||
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.__str__())
|
||||
|
||||
def __copy__(self):
|
||||
"""Generate a duplicate JID."""
|
||||
return JID(self)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
"""Generate a duplicate JID."""
|
||||
return JID(deepcopy(str(self), memo))
|
||||
return hash(_format_jid(self._node, self._domain, self._resource))
|
||||
|
||||
@@ -47,6 +47,7 @@ __all__ = [
|
||||
'xep_0108', # User Activity
|
||||
'xep_0115', # Entity Capabilities
|
||||
'xep_0118', # User Tune
|
||||
'xep_0122', # Data Forms Validation
|
||||
'xep_0128', # Extended Service Discovery
|
||||
'xep_0131', # Standard Headers and Internet Metadata
|
||||
'xep_0133', # Service Administration
|
||||
@@ -83,4 +84,5 @@ __all__ = [
|
||||
'xep_0319', # Last User Interaction in Presence
|
||||
'xep_0323', # IoT Systems Sensor Data
|
||||
'xep_0325', # IoT Systems Control
|
||||
'xep_0332', # HTTP Over XMPP Transport
|
||||
]
|
||||
|
||||
@@ -142,7 +142,6 @@ class PluginManager(object):
|
||||
:param dict config: Optional settings dictionary for
|
||||
configuring plugin behaviour.
|
||||
"""
|
||||
top_level = False
|
||||
if enabled is None:
|
||||
enabled = set()
|
||||
|
||||
@@ -166,14 +165,14 @@ class PluginManager(object):
|
||||
self.enable(dep, enabled=enabled)
|
||||
plugin._init()
|
||||
|
||||
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()
|
||||
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
|
||||
else:
|
||||
self._plugins[name].post_init()
|
||||
|
||||
def enable_all(self, names=None, config=None):
|
||||
"""Enable all registered plugins.
|
||||
|
||||
47
slixmpp/plugins/google/auth/stanza.py
Normal file
47
slixmpp/plugins/google/auth/stanza.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from slixmpp.xmlstream import ElementBase, ET
|
||||
|
||||
|
||||
class GoogleAuth(ElementBase):
|
||||
name = 'auth'
|
||||
namespace = 'http://www.google.com/talk/protocol/auth'
|
||||
plugin_attrib = 'google'
|
||||
interfaces = set(['client_uses_full_bind_result', 'service'])
|
||||
|
||||
discovery_attr= '{%s}client-uses-full-bind-result' % namespace
|
||||
service_attr= '{%s}service' % namespace
|
||||
|
||||
def setup(self, xml):
|
||||
"""Don't create XML for the plugin."""
|
||||
self.xml = ET.Element('')
|
||||
|
||||
def get_client_uses_full_bind_result(self):
|
||||
return self.parent()._get_attr(self.discovery_attr) == 'true'
|
||||
|
||||
def set_client_uses_full_bind_result(self, value):
|
||||
if value in (True, 'true'):
|
||||
self.parent()._set_attr(self.discovery_attr, 'true')
|
||||
else:
|
||||
self.parent()._del_attr(self.discovery_attr)
|
||||
|
||||
def del_client_uses_full_bind_result(self):
|
||||
self.parent()._del_attr(self.discovery_attr)
|
||||
|
||||
def get_service(self):
|
||||
return self.parent()._get_attr(self.service_attr, '')
|
||||
|
||||
def set_service(self, value):
|
||||
if value:
|
||||
self.parent()._set_attr(self.service_attr, value)
|
||||
else:
|
||||
self.parent()._del_attr(self.service_attr)
|
||||
|
||||
def del_service(self):
|
||||
self.parent()._del_attr(self.service_attr)
|
||||
90
slixmpp/plugins/google/gmail/notifications.py
Normal file
90
slixmpp/plugins/google/gmail/notifications.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from slixmpp.stanza import Iq
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.xmlstream.matcher import MatchXPath
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
from slixmpp.plugins import BasePlugin
|
||||
from slixmpp.plugins.google.gmail import stanza
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Gmail(BasePlugin):
|
||||
|
||||
"""
|
||||
Google: Gmail Notifications
|
||||
|
||||
Also see <https://developers.google.com/talk/jep_extensions/gmail>.
|
||||
"""
|
||||
|
||||
name = 'gmail'
|
||||
description = 'Google: Gmail Notifications'
|
||||
dependencies = set()
|
||||
stanza = stanza
|
||||
|
||||
def plugin_init(self):
|
||||
register_stanza_plugin(Iq, stanza.GmailQuery)
|
||||
register_stanza_plugin(Iq, stanza.MailBox)
|
||||
register_stanza_plugin(Iq, stanza.NewMail)
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('Gmail New Mail',
|
||||
MatchXPath('{%s}iq/{%s}%s' % (
|
||||
self.xmpp.default_ns,
|
||||
stanza.NewMail.namespace,
|
||||
stanza.NewMail.name)),
|
||||
self._handle_new_mail))
|
||||
|
||||
self._last_result_time = None
|
||||
self._last_result_tid = None
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp.remove_handler('Gmail New Mail')
|
||||
|
||||
def _handle_new_mail(self, iq):
|
||||
log.info('Gmail: New email!')
|
||||
iq.reply().send()
|
||||
self.xmpp.event('gmail_notification')
|
||||
|
||||
def check(self, timeout=None, callback=None):
|
||||
last_time = self._last_result_time
|
||||
last_tid = self._last_result_tid
|
||||
|
||||
callback = lambda iq: self._update_last_results(iq, callback)
|
||||
|
||||
return self.search(newer_time=last_time,
|
||||
newer_tid=last_tid,
|
||||
timeout=timeout,
|
||||
callback=callback)
|
||||
|
||||
def _update_last_results(self, iq, callback=None):
|
||||
self._last_result_time = iq['gmail_messages']['result_time']
|
||||
threads = iq['gmail_messages']['threads']
|
||||
if threads:
|
||||
self._last_result_tid = threads[0]['tid']
|
||||
if callback:
|
||||
callback(iq)
|
||||
|
||||
def search(self, query=None, newer_time=None, newer_tid=None,
|
||||
timeout=None, callback=None):
|
||||
if not query:
|
||||
log.info('Gmail: Checking for new email')
|
||||
else:
|
||||
log.info('Gmail: Searching for emails matching: "%s"', query)
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['to'] = self.xmpp.boundjid.bare
|
||||
iq['gmail']['search'] = query
|
||||
iq['gmail']['newer_than_time'] = newer_time
|
||||
iq['gmail']['newer_than_tid'] = newer_tid
|
||||
return iq.send(timeout=timeout, callback=callback)
|
||||
59
slixmpp/plugins/google/nosave/stanza.py
Normal file
59
slixmpp/plugins/google/nosave/stanza.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from slixmpp.jid import JID
|
||||
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
|
||||
|
||||
|
||||
class NoSave(ElementBase):
|
||||
name = 'x'
|
||||
namespace = 'google:nosave'
|
||||
plugin_attrib = 'google_nosave'
|
||||
interfaces = set(['value'])
|
||||
|
||||
def get_value(self):
|
||||
return self._get_attr('value', '') == 'enabled'
|
||||
|
||||
def set_value(self, value):
|
||||
self._set_attr('value', 'enabled' if value else 'disabled')
|
||||
|
||||
|
||||
class NoSaveQuery(ElementBase):
|
||||
name = 'query'
|
||||
namespace = 'google:nosave'
|
||||
plugin_attrib = 'google_nosave'
|
||||
interfaces = set()
|
||||
|
||||
|
||||
class Item(ElementBase):
|
||||
name = 'item'
|
||||
namespace = 'google:nosave'
|
||||
plugin_attrib = 'item'
|
||||
plugin_multi_attrib = 'items'
|
||||
interfaces = set(['jid', 'source', 'value'])
|
||||
|
||||
def get_value(self):
|
||||
return self._get_attr('value', '') == 'enabled'
|
||||
|
||||
def set_value(self, value):
|
||||
self._set_attr('value', 'enabled' if value else 'disabled')
|
||||
|
||||
def get_jid(self):
|
||||
return JID(self._get_attr('jid', ''))
|
||||
|
||||
def set_jid(self, value):
|
||||
self._set_attr('jid', str(value))
|
||||
|
||||
def get_source(self):
|
||||
return JID(self._get_attr('source', ''))
|
||||
|
||||
def set_source(self, value):
|
||||
self._set_attr('source', str(value))
|
||||
|
||||
|
||||
register_stanza_plugin(NoSaveQuery, Item)
|
||||
63
slixmpp/plugins/google/settings/settings.py
Normal file
63
slixmpp/plugins/google/settings/settings.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from slixmpp.stanza import Iq
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.xmlstream.matcher import StanzaPath
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
from slixmpp.plugins import BasePlugin
|
||||
from slixmpp.plugins.google.settings import stanza
|
||||
|
||||
|
||||
class GoogleSettings(BasePlugin):
|
||||
|
||||
"""
|
||||
Google: Gmail Notifications
|
||||
|
||||
Also see <https://developers.google.com/talk/jep_extensions/usersettings>.
|
||||
"""
|
||||
|
||||
name = 'google_settings'
|
||||
description = 'Google: User Settings'
|
||||
dependencies = set()
|
||||
stanza = stanza
|
||||
|
||||
def plugin_init(self):
|
||||
register_stanza_plugin(Iq, stanza.UserSettings)
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('Google Settings',
|
||||
StanzaPath('iq@type=set/google_settings'),
|
||||
self._handle_settings_change))
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp.remove_handler('Google Settings')
|
||||
|
||||
def get(self, timeout=None, callback=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq.enable('google_settings')
|
||||
return iq.send(timeout=timeout, callback=callback)
|
||||
|
||||
def update(self, settings, timeout=None, callback=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq.enable('google_settings')
|
||||
|
||||
for setting, value in settings.items():
|
||||
iq['google_settings'][setting] = value
|
||||
|
||||
return iq.send(timeout=timeout, callback=callback)
|
||||
|
||||
def _handle_settings_change(self, iq):
|
||||
reply = self.xmpp.Iq()
|
||||
reply['type'] = 'result'
|
||||
reply['id'] = iq['id']
|
||||
reply['to'] = iq['from']
|
||||
reply.send()
|
||||
self.xmpp.event('google_settings_change', iq)
|
||||
@@ -13,8 +13,9 @@ class FormField(ElementBase):
|
||||
namespace = 'jabber:x:data'
|
||||
name = 'field'
|
||||
plugin_attrib = 'field'
|
||||
plugin_multi_attrib = 'fields'
|
||||
interfaces = set(('answer', 'desc', 'required', 'value',
|
||||
'options', 'label', 'type', 'var'))
|
||||
'label', 'type', 'var'))
|
||||
sub_interfaces = set(('desc',))
|
||||
plugin_tag_map = {}
|
||||
plugin_attrib_map = {}
|
||||
@@ -165,6 +166,7 @@ class FieldOption(ElementBase):
|
||||
plugin_attrib = 'option'
|
||||
interfaces = set(('label', 'value'))
|
||||
sub_interfaces = set(('value',))
|
||||
plugin_multi_attrib = 'options'
|
||||
|
||||
|
||||
FormField.addOption = FormField.add_option
|
||||
|
||||
@@ -10,6 +10,7 @@ import copy
|
||||
import logging
|
||||
|
||||
from collections import OrderedDict
|
||||
from slixmpp.thirdparty import OrderedSet
|
||||
|
||||
from slixmpp.xmlstream import ElementBase, ET
|
||||
from slixmpp.plugins.xep_0004.stanza import FormField
|
||||
@@ -22,8 +23,7 @@ class Form(ElementBase):
|
||||
namespace = 'jabber:x:data'
|
||||
name = 'x'
|
||||
plugin_attrib = 'form'
|
||||
interfaces = set(('fields', 'instructions', 'items',
|
||||
'reported', 'title', 'type', 'values'))
|
||||
interfaces = OrderedSet(('instructions', 'reported', 'title', 'type', 'items', ))
|
||||
sub_interfaces = set(('title',))
|
||||
form_types = set(('cancel', 'form', 'result', 'submit'))
|
||||
|
||||
@@ -43,12 +43,12 @@ class Form(ElementBase):
|
||||
|
||||
@property
|
||||
def field(self):
|
||||
return self['fields']
|
||||
return self.get_fields()
|
||||
|
||||
def set_type(self, ftype):
|
||||
self._set_attr('type', ftype)
|
||||
if ftype == 'submit':
|
||||
fields = self['fields']
|
||||
fields = self.get_fields()
|
||||
for var in fields:
|
||||
field = fields[var]
|
||||
del field['type']
|
||||
@@ -74,7 +74,8 @@ class Form(ElementBase):
|
||||
field['desc'] = desc
|
||||
field['required'] = required
|
||||
if options is not None:
|
||||
field['options'] = options
|
||||
for option in options:
|
||||
field.add_option(**option)
|
||||
else:
|
||||
del field['type']
|
||||
self.append(field)
|
||||
@@ -151,7 +152,6 @@ class Form(ElementBase):
|
||||
return fields
|
||||
|
||||
def get_instructions(self):
|
||||
instructions = ''
|
||||
instsXML = self.xml.findall('{%s}instructions' % self.namespace)
|
||||
return "\n".join([instXML.text for instXML in instsXML])
|
||||
|
||||
@@ -170,7 +170,7 @@ class Form(ElementBase):
|
||||
def get_reported(self):
|
||||
fields = OrderedDict()
|
||||
xml = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,
|
||||
FormField.namespace))
|
||||
FormField.namespace))
|
||||
for field in xml:
|
||||
field = FormField(xml=field)
|
||||
fields[field['var']] = field
|
||||
@@ -178,7 +178,7 @@ class Form(ElementBase):
|
||||
|
||||
def get_values(self):
|
||||
values = OrderedDict()
|
||||
fields = self['fields']
|
||||
fields = self.get_fields()
|
||||
for var in fields:
|
||||
values[var] = fields[var]['value']
|
||||
return values
|
||||
@@ -195,7 +195,14 @@ class Form(ElementBase):
|
||||
fields = fields.items()
|
||||
for var, field in fields:
|
||||
field['var'] = var
|
||||
self.add_field(**field)
|
||||
self.add_field(
|
||||
var=field.get('var'),
|
||||
label=field.get('label'),
|
||||
desc=field.get('desc'),
|
||||
required=field.get('required'),
|
||||
value=field.get('value'),
|
||||
options=field.get('options'),
|
||||
type=field.get('type'))
|
||||
|
||||
def set_instructions(self, instructions):
|
||||
del self['instructions']
|
||||
@@ -213,17 +220,33 @@ class Form(ElementBase):
|
||||
self.add_item(item)
|
||||
|
||||
def set_reported(self, reported):
|
||||
"""
|
||||
This either needs a dictionary of dictionaries or a dictionary of form fields.
|
||||
:param reported:
|
||||
:return:
|
||||
"""
|
||||
for var in reported:
|
||||
field = reported[var]
|
||||
field['var'] = var
|
||||
self.add_reported(var, **field)
|
||||
|
||||
if isinstance(field, dict):
|
||||
self.add_reported(**field)
|
||||
else:
|
||||
reported = self.xml.find('{%s}reported' % self.namespace)
|
||||
if reported is None:
|
||||
reported = ET.Element('{%s}reported' % self.namespace)
|
||||
self.xml.append(reported)
|
||||
|
||||
fieldXML = ET.Element('{%s}field' % FormField.namespace)
|
||||
reported.append(fieldXML)
|
||||
new_field = FormField(xml=fieldXML)
|
||||
new_field.values = field.values
|
||||
|
||||
def set_values(self, values):
|
||||
fields = self['fields']
|
||||
fields = self.get_fields()
|
||||
for field in values:
|
||||
if field not in fields:
|
||||
if field not in self.get_fields():
|
||||
fields[field] = self.add_field(var=field)
|
||||
fields[field]['value'] = values[field]
|
||||
self.get_fields()[field]['value'] = values[field]
|
||||
|
||||
def merge(self, other):
|
||||
new = copy.copy(self)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from binding import py2xml, xml2py, xml2fault, fault2xml
|
||||
from slixmpp.plugins.xep_0009.binding import py2xml, xml2py, xml2fault, fault2xml
|
||||
from threading import RLock
|
||||
import abc
|
||||
import inspect
|
||||
@@ -18,6 +18,38 @@ import traceback
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def _isstr(obj):
|
||||
return isinstance(obj, str)
|
||||
|
||||
|
||||
# Class decorator to declare a metaclass to a class in a way compatible with Python 2 and 3.
|
||||
# This decorator is copied from 'six' (https://bitbucket.org/gutworth/six):
|
||||
#
|
||||
# Copyright (c) 2010-2015 Benjamin Peterson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
def _add_metaclass(metaclass):
|
||||
def wrapper(cls):
|
||||
orig_vars = cls.__dict__.copy()
|
||||
slots = orig_vars.get('__slots__')
|
||||
if slots is not None:
|
||||
if isinstance(slots, str):
|
||||
slots = [slots]
|
||||
for slots_var in slots:
|
||||
orig_vars.pop(slots_var)
|
||||
orig_vars.pop('__dict__', None)
|
||||
orig_vars.pop('__weakref__', None)
|
||||
return metaclass(cls.__name__, cls.__bases__, orig_vars)
|
||||
return wrapper
|
||||
|
||||
def _intercept(method, name, public):
|
||||
def _resolver(instance, *args, **kwargs):
|
||||
log.debug("Locally calling %s.%s with arguments %s.", instance.FQN(), method.__name__, args)
|
||||
@@ -68,7 +100,7 @@ def remote(function_argument, public = True):
|
||||
if hasattr(function_argument, '__call__'):
|
||||
return _intercept(function_argument, None, public)
|
||||
else:
|
||||
if not isinstance(function_argument, basestring):
|
||||
if not _isstr(function_argument):
|
||||
if not isinstance(function_argument, bool):
|
||||
raise Exception('Expected an RPC method name or visibility modifier!')
|
||||
else:
|
||||
@@ -222,12 +254,11 @@ class TimeoutException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@_add_metaclass(abc.ABCMeta)
|
||||
class Callback(object):
|
||||
'''
|
||||
A base class for callback handlers.
|
||||
'''
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
|
||||
@abc.abstractproperty
|
||||
def set_value(self, value):
|
||||
@@ -291,7 +322,7 @@ class Future(Callback):
|
||||
self._event.set()
|
||||
|
||||
|
||||
|
||||
@_add_metaclass(abc.ABCMeta)
|
||||
class Endpoint(object):
|
||||
'''
|
||||
The Endpoint class is an abstract base class for all objects
|
||||
@@ -303,8 +334,6 @@ class Endpoint(object):
|
||||
which specifies which object an RPC call refers to. It is the
|
||||
first part in a RPC method name '<fqn>.<method>'.
|
||||
'''
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
|
||||
def __init__(self, session, target_jid):
|
||||
'''
|
||||
@@ -491,7 +520,7 @@ class RemoteSession(object):
|
||||
|
||||
def _find_key(self, dict, value):
|
||||
"""return the key of dictionary dic given the value"""
|
||||
search = [k for k, v in dict.iteritems() if v == value]
|
||||
search = [k for k, v in dict.items() if v == value]
|
||||
if len(search) == 0:
|
||||
return None
|
||||
else:
|
||||
@@ -547,7 +576,7 @@ class RemoteSession(object):
|
||||
result = handler_cls(*args, **kwargs)
|
||||
Endpoint.__init__(result, self, self._client.boundjid.full)
|
||||
method_dict = result.get_methods()
|
||||
for method_name, method in method_dict.iteritems():
|
||||
for method_name, method in method_dict.items():
|
||||
#!!! self._client.plugin['xep_0009'].register_call(result.FQN(), method, method_name)
|
||||
self._register_call(result.FQN(), method, method_name)
|
||||
self._register_acl(result.FQN(), acl)
|
||||
@@ -569,11 +598,11 @@ class RemoteSession(object):
|
||||
self._register_callback(pid, callback)
|
||||
iq.send()
|
||||
|
||||
def close(self):
|
||||
def close(self, wait=False):
|
||||
'''
|
||||
Closes this session.
|
||||
'''
|
||||
self._client.disconnect(False)
|
||||
self._client.disconnect(wait=wait)
|
||||
self._session_close_callback()
|
||||
|
||||
def _on_jabber_rpc_method_call(self, iq):
|
||||
@@ -697,7 +726,8 @@ class Remote(object):
|
||||
if(client.boundjid.bare in cls._sessions):
|
||||
raise RemoteException("There already is a session associated with these credentials!")
|
||||
else:
|
||||
cls._sessions[client.boundjid.bare] = client;
|
||||
cls._sessions[client.boundjid.bare] = client
|
||||
|
||||
def _session_close_callback():
|
||||
with Remote._lock:
|
||||
del cls._sessions[client.boundjid.bare]
|
||||
|
||||
@@ -93,7 +93,8 @@ class XEP_0009(BasePlugin):
|
||||
|
||||
def _item_not_found(self, iq):
|
||||
payload = iq.get_payload()
|
||||
iq.reply().error().set_payload(payload)
|
||||
iq = iq.reply()
|
||||
iq.error().set_payload(payload)
|
||||
iq['error']['code'] = '404'
|
||||
iq['error']['type'] = 'cancel'
|
||||
iq['error']['condition'] = 'item-not-found'
|
||||
@@ -101,7 +102,8 @@ class XEP_0009(BasePlugin):
|
||||
|
||||
def _undefined_condition(self, iq):
|
||||
payload = iq.get_payload()
|
||||
iq.reply().error().set_payload(payload)
|
||||
iq = iq.reply()
|
||||
iq.error().set_payload(payload)
|
||||
iq['error']['code'] = '500'
|
||||
iq['error']['type'] = 'cancel'
|
||||
iq['error']['condition'] = 'undefined-condition'
|
||||
@@ -109,7 +111,8 @@ class XEP_0009(BasePlugin):
|
||||
|
||||
def _forbidden(self, iq):
|
||||
payload = iq.get_payload()
|
||||
iq.reply().error().set_payload(payload)
|
||||
iq = iq.reply()
|
||||
iq.error().set_payload(payload)
|
||||
iq['error']['code'] = '403'
|
||||
iq['error']['type'] = 'auth'
|
||||
iq['error']['condition'] = 'forbidden'
|
||||
@@ -117,7 +120,8 @@ class XEP_0009(BasePlugin):
|
||||
|
||||
def _recipient_unvailable(self, iq):
|
||||
payload = iq.get_payload()
|
||||
iq.reply().error().set_payload(payload)
|
||||
iq = iq.reply()
|
||||
error().set_payload(payload)
|
||||
iq['error']['code'] = '404'
|
||||
iq['error']['type'] = 'wait'
|
||||
iq['error']['condition'] = 'recipient-unavailable'
|
||||
@@ -216,3 +220,4 @@ class XEP_0009(BasePlugin):
|
||||
def _extract_method(self, stanza):
|
||||
xml = ET.fromstring("%s" % stanza)
|
||||
return xml.find("./methodCall/methodName").text
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from slixmpp.plugins import BasePlugin, register_plugin
|
||||
from slixmpp import Iq
|
||||
from slixmpp import future_wrapper, Iq
|
||||
from slixmpp.exceptions import XMPPError
|
||||
from slixmpp.xmlstream import JID, register_stanza_plugin
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
@@ -76,6 +76,7 @@ class XEP_0012(BasePlugin):
|
||||
def del_last_activity(self, jid):
|
||||
self.api['del_last_activity'](jid)
|
||||
|
||||
@future_wrapper
|
||||
def get_last_activity(self, jid, local=False, ifrom=None, timeout=None,
|
||||
callback=None, timeout_callback=None):
|
||||
if jid is not None and not isinstance(jid, JID):
|
||||
@@ -132,8 +133,7 @@ class XEP_0012(BasePlugin):
|
||||
if not isinstance(iq, Iq):
|
||||
reply = self.xmpp.Iq()
|
||||
else:
|
||||
iq.reply()
|
||||
reply = iq
|
||||
reply = iq.reply()
|
||||
|
||||
if jid not in self._last_activities:
|
||||
raise XMPPError('service-unavailable')
|
||||
|
||||
@@ -67,8 +67,7 @@ class XEP_0027(BasePlugin):
|
||||
register_stanza_plugin(Message, Encrypted)
|
||||
|
||||
self.xmpp.add_event_handler('unverified_signed_presence',
|
||||
self._handle_unverified_signed_presence,
|
||||
threaded=True)
|
||||
self._handle_unverified_signed_presence)
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('Signed Presence',
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import logging
|
||||
|
||||
from slixmpp import Iq
|
||||
from slixmpp import future_wrapper
|
||||
from slixmpp.plugins import BasePlugin
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.xmlstream.matcher import StanzaPath
|
||||
@@ -122,6 +123,12 @@ class XEP_0030(BasePlugin):
|
||||
for op in self._disco_ops:
|
||||
self.api.register(getattr(self.static, op), op, default=True)
|
||||
|
||||
def session_bind(self, jid):
|
||||
self.add_feature('http://jabber.org/protocol/disco#info')
|
||||
|
||||
def plugin_end(self):
|
||||
self.del_feature('http://jabber.org/protocol/disco#info')
|
||||
|
||||
def _add_disco_op(self, op, default_handler):
|
||||
self.api.register(default_handler, op)
|
||||
self.api.register_default(default_handler, op)
|
||||
@@ -288,6 +295,7 @@ class XEP_0030(BasePlugin):
|
||||
'cached': cached}
|
||||
return self.api['has_identity'](jid, node, ifrom, data)
|
||||
|
||||
@future_wrapper
|
||||
def get_info(self, jid=None, node=None, local=None,
|
||||
cached=None, **kwargs):
|
||||
"""
|
||||
@@ -362,9 +370,9 @@ class XEP_0030(BasePlugin):
|
||||
iq['to'] = jid
|
||||
iq['type'] = 'get'
|
||||
iq['disco_info']['node'] = node if node else ''
|
||||
iq.send(timeout=kwargs.get('timeout', None),
|
||||
callback=kwargs.get('callback', None),
|
||||
timeout_callback=kwargs.get('timeout_callback', None))
|
||||
return iq.send(timeout=kwargs.get('timeout', None),
|
||||
callback=kwargs.get('callback', None),
|
||||
timeout_callback=kwargs.get('timeout_callback', None))
|
||||
|
||||
def set_info(self, jid=None, node=None, info=None):
|
||||
"""
|
||||
@@ -375,6 +383,7 @@ class XEP_0030(BasePlugin):
|
||||
info = info['disco_info']
|
||||
self.api['set_info'](jid, node, None, info)
|
||||
|
||||
@future_wrapper
|
||||
def get_items(self, jid=None, node=None, local=False, **kwargs):
|
||||
"""
|
||||
Retrieve the disco#items results from a given JID/node combination.
|
||||
@@ -423,9 +432,9 @@ class XEP_0030(BasePlugin):
|
||||
raise NotImplementedError("XEP 0059 has not yet been fixed")
|
||||
return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
|
||||
else:
|
||||
iq.send(timeout=kwargs.get('timeout', None),
|
||||
callback=kwargs.get('callback', None),
|
||||
timeout_callback=kwargs.get('timeout_callback', None))
|
||||
return iq.send(timeout=kwargs.get('timeout', None),
|
||||
callback=kwargs.get('callback', None),
|
||||
timeout_callback=kwargs.get('timeout_callback', None))
|
||||
|
||||
def set_items(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
@@ -600,7 +609,7 @@ class XEP_0030(BasePlugin):
|
||||
"""
|
||||
self.api['del_features'](jid, node, None, kwargs)
|
||||
|
||||
def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}):
|
||||
def _run_node_handler(self, htype, jid, node=None, ifrom=None, data=None):
|
||||
"""
|
||||
Execute the most specific node handler for the given
|
||||
JID/node combination.
|
||||
@@ -611,6 +620,9 @@ class XEP_0030(BasePlugin):
|
||||
node -- The node requested.
|
||||
data -- Optional, custom data to pass to the handler.
|
||||
"""
|
||||
if not data:
|
||||
data = {}
|
||||
|
||||
return self.api[htype](jid, node, ifrom, data)
|
||||
|
||||
def _handle_disco_info(self, iq):
|
||||
@@ -634,7 +646,7 @@ class XEP_0030(BasePlugin):
|
||||
info['id'] = iq['id']
|
||||
info.send()
|
||||
else:
|
||||
iq.reply()
|
||||
iq = iq.reply()
|
||||
if info:
|
||||
info = self._fix_default_info(info)
|
||||
iq.set_payload(info.xml)
|
||||
@@ -674,7 +686,7 @@ class XEP_0030(BasePlugin):
|
||||
if isinstance(items, Iq):
|
||||
items.send()
|
||||
else:
|
||||
iq.reply()
|
||||
iq = iq.reply()
|
||||
if items:
|
||||
iq.set_payload(items.xml)
|
||||
iq.send()
|
||||
|
||||
@@ -120,7 +120,7 @@ class DiscoInfo(ElementBase):
|
||||
id_xml.attrib['{%s}lang' % self.xml_ns] = lang
|
||||
if name:
|
||||
id_xml.attrib['name'] = name
|
||||
self.xml.append(id_xml)
|
||||
self.xml.insert(0, id_xml)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -135,6 +135,12 @@ class XEP_0045(BasePlugin):
|
||||
'http://jabber.org/protocol/muc#user',
|
||||
'http://jabber.org/protocol/muc#user')), self.handle_groupchat_invite))
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp.plugin['xep_0030'].del_feature(feature='http://jabber.org/protocol/muc')
|
||||
|
||||
def session_bind(self, jid):
|
||||
self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/muc')
|
||||
|
||||
def handle_groupchat_invite(self, inv):
|
||||
""" Handle an invite into a muc.
|
||||
"""
|
||||
@@ -397,6 +403,16 @@ class XEP_0045(BasePlugin):
|
||||
return None
|
||||
return self.rooms[room].keys()
|
||||
|
||||
def getUsersByAffiliation(cls, room, affiliation='member', ifrom=None):
|
||||
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
|
||||
raise TypeError
|
||||
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
|
||||
item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation': affiliation})
|
||||
query.append(item)
|
||||
iq = cls.xmpp.Iq(sto=room, sfrom=ifrom, stype='get')
|
||||
iq.append(query)
|
||||
return iq.send()
|
||||
|
||||
|
||||
xep_0045 = XEP_0045
|
||||
register_plugin(XEP_0045)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from slixmpp import Message, Iq
|
||||
from slixmpp.exceptions import XMPPError
|
||||
@@ -23,17 +23,11 @@ class XEP_0047(BasePlugin):
|
||||
default_config = {
|
||||
'block_size': 4096,
|
||||
'max_block_size': 8192,
|
||||
'window_size': 1,
|
||||
'auto_accept': False,
|
||||
}
|
||||
|
||||
def plugin_init(self):
|
||||
self._streams = {}
|
||||
self._pending_streams = {}
|
||||
self._pending_lock = threading.Lock()
|
||||
self._stream_lock = threading.Lock()
|
||||
|
||||
self._preauthed_sids_lock = threading.Lock()
|
||||
self._preauthed_sids = {}
|
||||
|
||||
register_stanza_plugin(Iq, Open)
|
||||
@@ -85,9 +79,8 @@ class XEP_0047(BasePlugin):
|
||||
self._streams[(jid, sid, peer_jid)] = stream
|
||||
|
||||
def _del_stream(self, jid, sid, peer_jid, data):
|
||||
with self._stream_lock:
|
||||
if (jid, sid, peer_jid) in self._streams:
|
||||
del self._streams[(jid, sid, peer_jid)]
|
||||
if (jid, sid, peer_jid) in self._streams:
|
||||
del self._streams[(jid, sid, peer_jid)]
|
||||
|
||||
def _accept_stream(self, iq):
|
||||
receiver = iq['to']
|
||||
@@ -100,23 +93,20 @@ class XEP_0047(BasePlugin):
|
||||
|
||||
def _authorized(self, jid, sid, ifrom, iq):
|
||||
if self.auto_accept:
|
||||
if iq['ibb_open']['block_size'] <= self.max_block_size:
|
||||
return True
|
||||
return True
|
||||
return False
|
||||
|
||||
def _authorized_sid(self, jid, sid, ifrom, iq):
|
||||
with self._preauthed_sids_lock:
|
||||
if (jid, sid, ifrom) in self._preauthed_sids:
|
||||
del self._preauthed_sids[(jid, sid, ifrom)]
|
||||
return True
|
||||
return False
|
||||
if (jid, sid, ifrom) in self._preauthed_sids:
|
||||
del self._preauthed_sids[(jid, sid, ifrom)]
|
||||
return True
|
||||
return False
|
||||
|
||||
def _preauthorize_sid(self, jid, sid, ifrom, data):
|
||||
with self._preauthed_sids_lock:
|
||||
self._preauthed_sids[(jid, sid, ifrom)] = True
|
||||
self._preauthed_sids[(jid, sid, ifrom)] = True
|
||||
|
||||
def open_stream(self, jid, block_size=None, sid=None, window=1, use_messages=False,
|
||||
ifrom=None, block=True, timeout=None, callback=None):
|
||||
def open_stream(self, jid, block_size=None, sid=None, use_messages=False,
|
||||
ifrom=None, timeout=None, callback=None):
|
||||
if sid is None:
|
||||
sid = str(uuid.uuid4())
|
||||
if block_size is None:
|
||||
@@ -128,48 +118,28 @@ class XEP_0047(BasePlugin):
|
||||
iq['from'] = ifrom
|
||||
iq['ibb_open']['block_size'] = block_size
|
||||
iq['ibb_open']['sid'] = sid
|
||||
iq['ibb_open']['stanza'] = 'iq'
|
||||
iq['ibb_open']['stanza'] = 'message' if use_messages else 'iq'
|
||||
|
||||
stream = IBBytestream(self.xmpp, sid, block_size,
|
||||
iq['from'], iq['to'], window,
|
||||
use_messages)
|
||||
iq['from'], iq['to'], use_messages)
|
||||
|
||||
with self._stream_lock:
|
||||
self._pending_streams[iq['id']] = stream
|
||||
stream_future = asyncio.Future()
|
||||
|
||||
self._pending_streams[iq['id']] = stream
|
||||
|
||||
if block:
|
||||
resp = iq.send(timeout=timeout)
|
||||
self._handle_opened_stream(resp)
|
||||
return stream
|
||||
else:
|
||||
cb = None
|
||||
def _handle_opened_stream(iq):
|
||||
log.debug('IBB stream (%s) accepted by %s', stream.sid, iq['from'])
|
||||
stream.self_jid = iq['to']
|
||||
stream.peer_jid = iq['from']
|
||||
stream.stream_started = True
|
||||
self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream)
|
||||
stream_future.set_result(stream)
|
||||
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)
|
||||
callback(stream)
|
||||
self.xmpp.event('ibb_stream_start', stream)
|
||||
self.xmpp.event('stream:%s:%s' % (stream.sid, stream.peer_jid), stream)
|
||||
|
||||
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:
|
||||
log.debug('IBB stream (%s) accepted by %s', stream.sid, iq['from'])
|
||||
stream.self_jid = iq['to']
|
||||
stream.peer_jid = iq['from']
|
||||
stream.stream_started.set()
|
||||
self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream)
|
||||
self.xmpp.event('ibb_stream_start', stream)
|
||||
self.xmpp.event('stream:%s:%s' % (stream.sid, stream.peer_jid), stream)
|
||||
iq.send(timeout=timeout, callback=_handle_opened_stream)
|
||||
|
||||
with self._stream_lock:
|
||||
if iq['id'] in self._pending_streams:
|
||||
del self._pending_streams[iq['id']]
|
||||
return stream_future
|
||||
|
||||
def _handle_open_request(self, iq):
|
||||
sid = iq['ibb_open']['sid']
|
||||
@@ -181,18 +151,16 @@ class XEP_0047(BasePlugin):
|
||||
raise XMPPError(etype='modify', condition='bad-request')
|
||||
|
||||
if not self._accept_stream(iq):
|
||||
raise XMPPError(etype='modify', condition='not-acceptable')
|
||||
raise XMPPError(etype='cancel', condition='not-acceptable')
|
||||
|
||||
if size > self.max_block_size:
|
||||
raise XMPPError('resource-constraint')
|
||||
|
||||
stream = IBBytestream(self.xmpp, sid, size,
|
||||
iq['to'], iq['from'],
|
||||
self.window_size)
|
||||
stream.stream_started.set()
|
||||
iq['to'], iq['from'])
|
||||
stream.stream_started = True
|
||||
self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream)
|
||||
iq.reply()
|
||||
iq.send()
|
||||
iq.reply().send()
|
||||
|
||||
self.xmpp.event('ibb_stream_start', stream)
|
||||
self.xmpp.event('stream:%s:%s' % (sid, stream.peer_jid), stream)
|
||||
|
||||
@@ -24,7 +24,7 @@ class Open(ElementBase):
|
||||
interfaces = set(('block_size', 'sid', 'stanza'))
|
||||
|
||||
def get_block_size(self):
|
||||
return int(self._get_attr('block-size'))
|
||||
return int(self._get_attr('block-size', '0'))
|
||||
|
||||
def set_block_size(self, value):
|
||||
self._set_attr('block-size', str(value))
|
||||
@@ -47,7 +47,10 @@ class Data(ElementBase):
|
||||
self._set_attr('seq', str(value))
|
||||
|
||||
def get_data(self):
|
||||
b64_data = self.xml.text.strip()
|
||||
text = self.xml.text
|
||||
if not text:
|
||||
raise XMPPError('not-acceptable', 'IBB data element is empty.')
|
||||
b64_data = text.strip()
|
||||
if VALID_B64.match(b64_data).group() == b64_data:
|
||||
return from_b64(b64_data)
|
||||
else:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import socket
|
||||
import threading
|
||||
import logging
|
||||
from queue import Queue
|
||||
|
||||
from slixmpp.stanza import Iq
|
||||
from slixmpp.exceptions import XMPPError
|
||||
@@ -12,11 +11,10 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class IBBytestream(object):
|
||||
|
||||
def __init__(self, xmpp, sid, block_size, jid, peer, window_size=1, use_messages=False):
|
||||
def __init__(self, xmpp, sid, block_size, jid, peer, use_messages=False):
|
||||
self.xmpp = xmpp
|
||||
self.sid = sid
|
||||
self.block_size = block_size
|
||||
self.window_size = window_size
|
||||
self.use_messages = use_messages
|
||||
|
||||
if jid is None:
|
||||
@@ -27,29 +25,20 @@ class IBBytestream(object):
|
||||
self.send_seq = -1
|
||||
self.recv_seq = -1
|
||||
|
||||
self._send_seq_lock = threading.Lock()
|
||||
self._recv_seq_lock = threading.Lock()
|
||||
self.stream_started = False
|
||||
self.stream_in_closed = False
|
||||
self.stream_out_closed = False
|
||||
|
||||
self.stream_started = threading.Event()
|
||||
self.stream_in_closed = threading.Event()
|
||||
self.stream_out_closed = threading.Event()
|
||||
self.recv_queue = asyncio.Queue()
|
||||
|
||||
self.recv_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():
|
||||
@asyncio.coroutine
|
||||
def send(self, data, timeout=None):
|
||||
if not self.stream_started or self.stream_out_closed:
|
||||
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
|
||||
if len(data) > self.block_size:
|
||||
data = data[:self.block_size]
|
||||
self.send_seq = (self.send_seq + 1) % 65535
|
||||
seq = self.send_seq
|
||||
if self.use_messages:
|
||||
msg = self.xmpp.Message()
|
||||
msg['to'] = self.peer_jid
|
||||
@@ -59,7 +48,6 @@ class IBBytestream(object):
|
||||
msg['ibb_data']['seq'] = seq
|
||||
msg['ibb_data']['data'] = data
|
||||
msg.send()
|
||||
self.send_window.release()
|
||||
else:
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
@@ -68,74 +56,66 @@ class IBBytestream(object):
|
||||
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)
|
||||
yield from iq.send(timeout=timeout)
|
||||
return len(data)
|
||||
|
||||
def sendall(self, data):
|
||||
@asyncio.coroutine
|
||||
def sendall(self, data, timeout=None):
|
||||
sent_len = 0
|
||||
while sent_len < len(data):
|
||||
sent_len += self.send(data[sent_len:])
|
||||
sent_len += yield from self.send(data[sent_len:self.block_size], timeout=timeout)
|
||||
|
||||
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()
|
||||
@asyncio.coroutine
|
||||
def sendfile(self, file, timeout=None):
|
||||
while True:
|
||||
data = file.read(self.block_size)
|
||||
if not data:
|
||||
break
|
||||
yield from self.send(data, timeout=timeout)
|
||||
|
||||
def _recv_data(self, stanza):
|
||||
with self._recv_seq_lock:
|
||||
new_seq = stanza['ibb_data']['seq']
|
||||
if new_seq != (self.recv_seq + 1) % 65535:
|
||||
self.close()
|
||||
raise XMPPError('unexpected-request')
|
||||
self.recv_seq = new_seq
|
||||
new_seq = stanza['ibb_data']['seq']
|
||||
if new_seq != (self.recv_seq + 1) % 65535:
|
||||
self.close()
|
||||
raise XMPPError('unexpected-request')
|
||||
self.recv_seq = new_seq
|
||||
|
||||
data = stanza['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})
|
||||
self.recv_queue.put_nowait(data)
|
||||
self.xmpp.event('ibb_stream_data', self)
|
||||
|
||||
if isinstance(stanza, Iq):
|
||||
stanza.reply()
|
||||
stanza.send()
|
||||
stanza.reply().send()
|
||||
|
||||
def recv(self, *args, **kwargs):
|
||||
return self.read(block=True)
|
||||
return self.read()
|
||||
|
||||
def read(self, block=True, timeout=None, **kwargs):
|
||||
if not self.stream_started.is_set() or \
|
||||
self.stream_in_closed.is_set():
|
||||
def read(self):
|
||||
if not self.stream_started or self.stream_in_closed:
|
||||
raise socket.error
|
||||
if timeout is not None:
|
||||
block = True
|
||||
try:
|
||||
return self.recv_queue.get(block, timeout)
|
||||
except:
|
||||
return None
|
||||
return self.recv_queue.get_nowait()
|
||||
|
||||
def close(self):
|
||||
def close(self, timeout=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq['to'] = self.peer_jid
|
||||
iq['from'] = self.self_jid
|
||||
iq['ibb_close']['sid'] = self.sid
|
||||
self.stream_out_closed.set()
|
||||
iq.send(block=False,
|
||||
callback=lambda x: self.stream_in_closed.set())
|
||||
self.stream_out_closed = True
|
||||
def _close_stream(_):
|
||||
self.stream_in_closed = True
|
||||
future = iq.send(timeout=timeout, callback=_close_stream)
|
||||
self.xmpp.event('ibb_stream_end', self)
|
||||
return future
|
||||
|
||||
def _closed(self, iq):
|
||||
self.stream_in_closed.set()
|
||||
self.stream_out_closed.set()
|
||||
iq.reply()
|
||||
iq.send()
|
||||
self.stream_in_closed = True
|
||||
self.stream_out_closed = True
|
||||
iq.reply().send()
|
||||
self.xmpp.event('ibb_stream_end', self)
|
||||
|
||||
def makefile(self, *args, **kwargs):
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
import time
|
||||
|
||||
from slixmpp import Iq
|
||||
from slixmpp.exceptions import IqError
|
||||
from slixmpp.exceptions import XMPPError
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.xmlstream.matcher import StanzaPath
|
||||
from slixmpp.xmlstream import register_stanza_plugin, JID
|
||||
@@ -37,10 +37,6 @@ class XEP_0050(BasePlugin):
|
||||
|
||||
Also see <http://xmpp.org/extensions/xep-0050.html>
|
||||
|
||||
Configuration Values:
|
||||
threaded -- Indicates if command events should be threaded.
|
||||
Defaults to True.
|
||||
|
||||
Events:
|
||||
command_execute -- Received a command with action="execute"
|
||||
command_next -- Received a command with action="next"
|
||||
@@ -48,8 +44,6 @@ class XEP_0050(BasePlugin):
|
||||
command_cancel -- Received a command with action="cancel"
|
||||
|
||||
Attributes:
|
||||
threaded -- Indicates if command events should be threaded.
|
||||
Defaults to True.
|
||||
commands -- A dictionary mapping JID/node pairs to command
|
||||
names and handlers.
|
||||
sessions -- A dictionary or equivalent backend mapping
|
||||
@@ -83,7 +77,6 @@ class XEP_0050(BasePlugin):
|
||||
dependencies = set(['xep_0030', 'xep_0004'])
|
||||
stanza = stanza
|
||||
default_config = {
|
||||
'threaded': True,
|
||||
'session_db': None
|
||||
}
|
||||
|
||||
@@ -101,7 +94,7 @@ class XEP_0050(BasePlugin):
|
||||
self._handle_command))
|
||||
|
||||
register_stanza_plugin(Iq, Command)
|
||||
register_stanza_plugin(Command, Form)
|
||||
register_stanza_plugin(Command, Form, iterable=True)
|
||||
|
||||
self.xmpp.add_event_handler('command_execute',
|
||||
self._handle_command_start)
|
||||
@@ -223,6 +216,7 @@ class XEP_0050(BasePlugin):
|
||||
name, handler = self.commands.get(key, ('Not found', None))
|
||||
if not handler:
|
||||
log.debug('Command not found: %s, %s', key, self.commands)
|
||||
raise XMPPError('item-not-found')
|
||||
|
||||
payload = []
|
||||
for stanza in iq['command']['substanzas']:
|
||||
@@ -339,7 +333,7 @@ class XEP_0050(BasePlugin):
|
||||
for item in payload:
|
||||
register_stanza_plugin(Command, item.__class__, iterable=True)
|
||||
|
||||
iq.reply()
|
||||
iq = iq.reply()
|
||||
iq['command']['node'] = session['node']
|
||||
iq['command']['sessionid'] = session['id']
|
||||
|
||||
@@ -382,7 +376,7 @@ class XEP_0050(BasePlugin):
|
||||
if handler:
|
||||
handler(iq, session)
|
||||
del self.sessions[sessionid]
|
||||
iq.reply()
|
||||
iq = iq.reply()
|
||||
iq['command']['node'] = node
|
||||
iq['command']['sessionid'] = sessionid
|
||||
iq['command']['status'] = 'canceled'
|
||||
@@ -421,12 +415,26 @@ class XEP_0050(BasePlugin):
|
||||
|
||||
del self.sessions[sessionid]
|
||||
|
||||
iq.reply()
|
||||
payload = session['payload']
|
||||
if payload is None:
|
||||
payload = []
|
||||
if not isinstance(payload, list):
|
||||
payload = [payload]
|
||||
|
||||
for item in payload:
|
||||
register_stanza_plugin(Command, item.__class__, iterable=True)
|
||||
|
||||
iq = iq.reply()
|
||||
|
||||
iq['command']['node'] = node
|
||||
iq['command']['sessionid'] = sessionid
|
||||
iq['command']['actions'] = []
|
||||
iq['command']['status'] = 'completed'
|
||||
iq['command']['notes'] = session['notes']
|
||||
|
||||
for item in payload:
|
||||
iq['command'].append(item)
|
||||
|
||||
iq.send()
|
||||
else:
|
||||
raise XMPPError('item-not-found')
|
||||
|
||||
@@ -128,7 +128,8 @@ class Telephone(ElementBase):
|
||||
|
||||
def setup(self, xml=None):
|
||||
super(Telephone, self).setup(xml=xml)
|
||||
self._set_sub_text('NUMBER', '', keep=True)
|
||||
## this blanks out numbers received from server
|
||||
##self._set_sub_text('NUMBER', '', keep=True)
|
||||
|
||||
def set_number(self, value):
|
||||
self._set_sub_text('NUMBER', value, keep=True)
|
||||
@@ -324,7 +325,10 @@ class Birthday(ElementBase):
|
||||
def get_bday(self):
|
||||
if not self.xml.text:
|
||||
return None
|
||||
return xep_0082.parse(self.xml.text)
|
||||
try:
|
||||
return xep_0082.parse(self.xml.text)
|
||||
except ValueError:
|
||||
return self.xml.text
|
||||
|
||||
|
||||
class Rev(ElementBase):
|
||||
@@ -343,7 +347,10 @@ class Rev(ElementBase):
|
||||
def get_rev(self):
|
||||
if not self.xml.text:
|
||||
return None
|
||||
return xep_0082.parse(self.xml.text)
|
||||
try:
|
||||
return xep_0082.parse(self.xml.text)
|
||||
except ValueError:
|
||||
return self.xml.text
|
||||
|
||||
|
||||
class Title(ElementBase):
|
||||
@@ -523,8 +530,11 @@ class TimeZone(ElementBase):
|
||||
def get_tz(self):
|
||||
if not self.xml.text:
|
||||
return xep_0082.tzutc()
|
||||
time = xep_0082.parse('00:00:00%s' % self.xml.text)
|
||||
return time.tzinfo
|
||||
try:
|
||||
time = xep_0082.parse('00:00:00%s' % self.xml.text)
|
||||
return time.tzinfo
|
||||
except ValueError:
|
||||
return self.xml.text
|
||||
|
||||
|
||||
register_stanza_plugin(VCardTemp, Name)
|
||||
|
||||
@@ -15,6 +15,7 @@ from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.xmlstream.matcher import StanzaPath
|
||||
from slixmpp.plugins import BasePlugin
|
||||
from slixmpp.plugins.xep_0054 import VCardTemp, stanza
|
||||
from slixmpp import future_wrapper
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -59,8 +60,9 @@ class XEP_0054(BasePlugin):
|
||||
def make_vcard(self):
|
||||
return VCardTemp()
|
||||
|
||||
@future_wrapper
|
||||
def get_vcard(self, jid=None, ifrom=None, local=None, cached=False,
|
||||
block=True, callback=None, timeout=None):
|
||||
callback=None, timeout=None, timeout_callback=None):
|
||||
if local is None:
|
||||
if jid is not None and not isinstance(jid, JID):
|
||||
jid = JID(jid)
|
||||
@@ -99,14 +101,12 @@ class XEP_0054(BasePlugin):
|
||||
iq['type'] = 'get'
|
||||
iq.enable('vcard_temp')
|
||||
|
||||
vcard = iq.send(block=block, callback=callback, timeout=timeout)
|
||||
return iq.send(callback=callback, timeout=timeout,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
if block:
|
||||
self.api['set_vcard'](vcard['from'], args=vcard['vcard_temp'])
|
||||
return vcard
|
||||
|
||||
def publish_vcard(self, vcard=None, jid=None, block=True, ifrom=None,
|
||||
callback=None, timeout=None):
|
||||
@future_wrapper
|
||||
def publish_vcard(self, vcard=None, jid=None, ifrom=None,
|
||||
callback=None, timeout=None, timeout_callback=None):
|
||||
self.api['set_vcard'](jid, None, ifrom, vcard)
|
||||
if self.xmpp.is_component:
|
||||
return
|
||||
@@ -116,7 +116,8 @@ class XEP_0054(BasePlugin):
|
||||
iq['from'] = ifrom
|
||||
iq['type'] = 'set'
|
||||
iq.append(vcard)
|
||||
return iq.send(block=block, callback=callback, timeout=timeout)
|
||||
return iq.send(callback=callback, timeout=timeout,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
def _handle_get_vcard(self, iq):
|
||||
if iq['type'] == 'result':
|
||||
@@ -127,7 +128,7 @@ class XEP_0054(BasePlugin):
|
||||
if isinstance(vcard, Iq):
|
||||
vcard.send()
|
||||
else:
|
||||
iq.reply()
|
||||
iq = iq.reply()
|
||||
iq.append(vcard)
|
||||
iq.send()
|
||||
elif iq['type'] == 'set':
|
||||
|
||||
@@ -260,12 +260,12 @@ class XEP_0060(BasePlugin):
|
||||
|
||||
Arguments:
|
||||
jid -- The pubsub service JID.
|
||||
node -- The node to subscribe to.
|
||||
node -- The node to unsubscribe from.
|
||||
subid -- The specific subscription, if multiple subscriptions
|
||||
exist for this JID/node combination.
|
||||
bare -- Indicates if the subscribee is a bare or full JID.
|
||||
Defaults to True for a bare JID.
|
||||
subscribee -- The JID that is subscribing to the node.
|
||||
subscribee -- The JID that is unsubscribing from the node.
|
||||
ifrom -- Specify the sender's JID.
|
||||
timeout -- The length of time (in seconds) to wait for a
|
||||
response before exiting the send call if blocking
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from slixmpp.plugins.base import register_plugin
|
||||
|
||||
from slixmpp.plugins.xep_0065.socks5 import Socks5Protocol
|
||||
from slixmpp.plugins.xep_0065.stanza import Socks5
|
||||
from slixmpp.plugins.xep_0065.proxy import XEP_0065
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
import socket
|
||||
|
||||
from hashlib import sha1
|
||||
from uuid import uuid4
|
||||
|
||||
from slixmpp.thirdparty.socks import socksocket, PROXY_TYPE_SOCKS5
|
||||
|
||||
from slixmpp.stanza import Iq
|
||||
from slixmpp.exceptions import XMPPError
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
@@ -14,7 +12,7 @@ from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.xmlstream.matcher import StanzaPath
|
||||
from slixmpp.plugins.base import BasePlugin
|
||||
|
||||
from slixmpp.plugins.xep_0065 import stanza, Socks5
|
||||
from slixmpp.plugins.xep_0065 import stanza, Socks5, Socks5Protocol
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -23,7 +21,7 @@ log = logging.getLogger(__name__)
|
||||
class XEP_0065(BasePlugin):
|
||||
|
||||
name = 'xep_0065'
|
||||
description = "Socks5 Bytestreams"
|
||||
description = "XEP-0065: SOCKS5 Bytestreams"
|
||||
dependencies = set(['xep_0030'])
|
||||
default_config = {
|
||||
'auto_accept': False
|
||||
@@ -34,9 +32,6 @@ class XEP_0065(BasePlugin):
|
||||
|
||||
self._proxies = {}
|
||||
self._sessions = {}
|
||||
self._sessions_lock = threading.Lock()
|
||||
|
||||
self._preauthed_sids_lock = threading.Lock()
|
||||
self._preauthed_sids = {}
|
||||
|
||||
self.xmpp.register_handler(
|
||||
@@ -65,34 +60,34 @@ class XEP_0065(BasePlugin):
|
||||
connection.
|
||||
"""
|
||||
if not self._proxies:
|
||||
self._proxies = self.discover_proxies()
|
||||
self._proxies = yield from self.discover_proxies()
|
||||
|
||||
if sid is None:
|
||||
sid = uuid4().hex
|
||||
|
||||
used = self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout)
|
||||
used = yield from self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout)
|
||||
proxy = used['socks']['streamhost_used']['jid']
|
||||
|
||||
if proxy not in self._proxies:
|
||||
log.warning('Received unknown SOCKS5 proxy: %s', proxy)
|
||||
return
|
||||
|
||||
with self._sessions_lock:
|
||||
self._sessions[sid] = self._connect_proxy(
|
||||
sid,
|
||||
self.xmpp.boundjid,
|
||||
to,
|
||||
try:
|
||||
self._sessions[sid] = (yield from self._connect_proxy(
|
||||
self._get_dest_sha1(sid, self.xmpp.boundjid, to),
|
||||
self._proxies[proxy][0],
|
||||
self._proxies[proxy][1],
|
||||
peer=to)
|
||||
self._proxies[proxy][1]))[1]
|
||||
except socket.error:
|
||||
return None
|
||||
addr, port = yield from self._sessions[sid].connected
|
||||
|
||||
# Request that the proxy activate the session with the target.
|
||||
self.activate(proxy, sid, to, timeout=timeout)
|
||||
socket = self.get_socket(sid)
|
||||
self.xmpp.event('stream:%s:%s' % (sid, to), socket)
|
||||
return socket
|
||||
yield from self.activate(proxy, sid, to, timeout=timeout)
|
||||
sock = self.get_socket(sid)
|
||||
self.xmpp.event('stream:%s:%s' % (sid, to), sock)
|
||||
return sock
|
||||
|
||||
def request_stream(self, to, sid=None, ifrom=None, block=True, timeout=None, callback=None):
|
||||
def request_stream(self, to, sid=None, ifrom=None, timeout=None, callback=None):
|
||||
if sid is None:
|
||||
sid = uuid4().hex
|
||||
|
||||
@@ -107,7 +102,7 @@ class XEP_0065(BasePlugin):
|
||||
iq['socks']['sid'] = sid
|
||||
for proxy, (host, port) in self._proxies.items():
|
||||
iq['socks'].add_streamhost(proxy, host, port)
|
||||
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||
return iq.send(timeout=timeout, callback=callback)
|
||||
|
||||
def discover_proxies(self, jid=None, ifrom=None, timeout=None):
|
||||
"""Auto-discover the JIDs of SOCKS5 proxies on an XMPP server."""
|
||||
@@ -119,11 +114,16 @@ class XEP_0065(BasePlugin):
|
||||
|
||||
discovered = set()
|
||||
|
||||
disco_items = self.xmpp['xep_0030'].get_items(jid, timeout=timeout)
|
||||
disco_items = yield from self.xmpp['xep_0030'].get_items(jid, timeout=timeout)
|
||||
disco_items = {item[0] for item in disco_items['disco_items']['items']}
|
||||
|
||||
for item in disco_items['disco_items']['items']:
|
||||
disco_info_futures = {}
|
||||
for item in disco_items:
|
||||
disco_info_futures[item] = self.xmpp['xep_0030'].get_info(item, timeout=timeout)
|
||||
|
||||
for item in disco_items:
|
||||
try:
|
||||
disco_info = self.xmpp['xep_0030'].get_info(item[0], timeout=timeout)
|
||||
disco_info = yield from disco_info_futures[item]
|
||||
except XMPPError:
|
||||
continue
|
||||
else:
|
||||
@@ -135,7 +135,7 @@ class XEP_0065(BasePlugin):
|
||||
|
||||
for jid in discovered:
|
||||
try:
|
||||
addr = self.get_network_address(jid, ifrom=ifrom, timeout=timeout)
|
||||
addr = yield from self.get_network_address(jid, ifrom=ifrom, timeout=timeout)
|
||||
self._proxies[jid] = (addr['socks']['streamhost']['host'],
|
||||
addr['socks']['streamhost']['port'])
|
||||
except XMPPError:
|
||||
@@ -143,11 +143,20 @@ class XEP_0065(BasePlugin):
|
||||
|
||||
return self._proxies
|
||||
|
||||
def get_network_address(self, proxy, ifrom=None, block=True, timeout=None, callback=None):
|
||||
def get_network_address(self, proxy, ifrom=None, timeout=None, callback=None):
|
||||
"""Get the network information of a proxy."""
|
||||
iq = self.xmpp.Iq(sto=proxy, stype='get', sfrom=ifrom)
|
||||
iq.enable('socks')
|
||||
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||
return iq.send(timeout=timeout, callback=callback)
|
||||
|
||||
def _get_dest_sha1(self, sid, requester, target):
|
||||
# The hostname MUST be SHA1(SID + Requester JID + Target JID)
|
||||
# where the output is hexadecimal-encoded (not binary).
|
||||
digest = sha1()
|
||||
digest.update(sid.encode('utf8'))
|
||||
digest.update(str(requester).encode('utf8'))
|
||||
digest.update(str(target).encode('utf8'))
|
||||
return digest.hexdigest()
|
||||
|
||||
def _handle_streamhost(self, iq):
|
||||
"""Handle incoming SOCKS5 session request."""
|
||||
@@ -159,40 +168,59 @@ class XEP_0065(BasePlugin):
|
||||
raise XMPPError(etype='modify', condition='not-acceptable')
|
||||
|
||||
streamhosts = iq['socks']['streamhosts']
|
||||
conn = None
|
||||
used_streamhost = None
|
||||
requester = iq['from']
|
||||
target = iq['to']
|
||||
|
||||
sender = iq['from']
|
||||
dest = self._get_dest_sha1(sid, requester, target)
|
||||
|
||||
proxy_futures = []
|
||||
for streamhost in streamhosts:
|
||||
try:
|
||||
conn = self._connect_proxy(sid,
|
||||
sender,
|
||||
self.xmpp.boundjid,
|
||||
proxy_futures.append(self._connect_proxy(
|
||||
dest,
|
||||
streamhost['host'],
|
||||
streamhost['port'],
|
||||
peer=sender)
|
||||
streamhost['port']))
|
||||
|
||||
@asyncio.coroutine
|
||||
def gather(futures, iq, streamhosts):
|
||||
proxies = yield from asyncio.gather(*futures, return_exceptions=True)
|
||||
for streamhost, proxy in zip(streamhosts, proxies):
|
||||
if isinstance(proxy, ValueError):
|
||||
continue
|
||||
elif isinstance(proxy, socket.error):
|
||||
log.error('Socket error while connecting to the proxy.')
|
||||
continue
|
||||
proxy = proxy[1]
|
||||
# TODO: what if the future never happens?
|
||||
try:
|
||||
addr, port = yield from proxy.connected
|
||||
except socket.error:
|
||||
log.exception('Socket error while connecting to the proxy.')
|
||||
continue
|
||||
# TODO: make a better choice than just the first working one.
|
||||
used_streamhost = streamhost['jid']
|
||||
conn = proxy
|
||||
break
|
||||
except socket.error:
|
||||
continue
|
||||
else:
|
||||
raise XMPPError(etype='cancel', condition='item-not-found')
|
||||
else:
|
||||
raise XMPPError(etype='cancel', condition='item-not-found')
|
||||
|
||||
iq.reply()
|
||||
with self._sessions_lock:
|
||||
# TODO: close properly the connection to the other proxies.
|
||||
|
||||
iq = iq.reply()
|
||||
self._sessions[sid] = conn
|
||||
iq['socks']['sid'] = sid
|
||||
iq['socks']['streamhost_used']['jid'] = used_streamhost
|
||||
iq.send()
|
||||
self.xmpp.event('socks5_stream', conn)
|
||||
self.xmpp.event('stream:%s:%s' % (sid, conn.peer_jid), conn)
|
||||
iq['socks']['sid'] = sid
|
||||
iq['socks']['streamhost_used']['jid'] = used_streamhost
|
||||
iq.send()
|
||||
self.xmpp.event('socks5_stream', conn)
|
||||
self.xmpp.event('stream:%s:%s' % (sid, requester), conn)
|
||||
|
||||
def activate(self, proxy, sid, target, ifrom=None, block=True, timeout=None, callback=None):
|
||||
asyncio.async(gather(proxy_futures, iq, streamhosts))
|
||||
|
||||
def activate(self, proxy, sid, target, ifrom=None, timeout=None, callback=None):
|
||||
"""Activate the socks5 session that has been negotiated."""
|
||||
iq = self.xmpp.Iq(sto=proxy, stype='set', sfrom=ifrom)
|
||||
iq['socks']['sid'] = sid
|
||||
iq['socks']['activate'] = target
|
||||
iq.send(block=block, timeout=timeout, callback=callback)
|
||||
return iq.send(timeout=timeout, callback=callback)
|
||||
|
||||
def deactivate(self, sid):
|
||||
"""Closes the proxy socket associated with this SID."""
|
||||
@@ -204,66 +232,27 @@ class XEP_0065(BasePlugin):
|
||||
except socket.error:
|
||||
pass
|
||||
# Though this should not be neccessary remove the closed session anyway
|
||||
with self._sessions_lock:
|
||||
if sid in self._sessions:
|
||||
log.warn(('SOCKS5 session with sid = "%s" was not ' +
|
||||
'removed from _sessions by sock.close()') % sid)
|
||||
del self._sessions[sid]
|
||||
if sid in self._sessions:
|
||||
log.warn(('SOCKS5 session with sid = "%s" was not ' +
|
||||
'removed from _sessions by sock.close()') % sid)
|
||||
del self._sessions[sid]
|
||||
|
||||
def close(self):
|
||||
"""Closes all proxy sockets."""
|
||||
for sid, sock in self._sessions.items():
|
||||
sock.close()
|
||||
with self._sessions_lock:
|
||||
self._sessions = {}
|
||||
self._sessions = {}
|
||||
|
||||
def _connect_proxy(self, sid, requester, target, proxy, proxy_port, peer=None):
|
||||
""" Establishes a connection between the client and the server-side
|
||||
def _connect_proxy(self, dest, proxy, proxy_port):
|
||||
""" Returns a future to a connection between the client and the server-side
|
||||
Socks5 proxy.
|
||||
|
||||
sid : The StreamID. <str>
|
||||
requester : The JID of the requester. <str>
|
||||
target : The JID of the target. <str>
|
||||
proxy_host : The hostname or the IP of the proxy. <str>
|
||||
proxy_port : The port of the proxy. <str> or <int>
|
||||
peer : The JID for the other side of the stream, regardless
|
||||
of target or requester status.
|
||||
dest : The SHA-1 of (SID + Requester JID + Target JID), in hex. <str>
|
||||
host : The hostname or the IP of the proxy. <str>
|
||||
port : The port of the proxy. <str> or <int>
|
||||
"""
|
||||
# Because the xep_0065 plugin uses the proxy_port as string,
|
||||
# the Proxy class accepts the proxy_port argument as a string
|
||||
# or an integer. Here, we force to use the port as an integer.
|
||||
proxy_port = int(proxy_port)
|
||||
|
||||
sock = socksocket()
|
||||
sock.setproxy(PROXY_TYPE_SOCKS5, proxy, port=proxy_port)
|
||||
|
||||
# The hostname MUST be SHA1(SID + Requester JID + Target JID)
|
||||
# where the output is hexadecimal-encoded (not binary).
|
||||
digest = sha1()
|
||||
digest.update(sid)
|
||||
digest.update(str(requester))
|
||||
digest.update(str(target))
|
||||
|
||||
dest = digest.hexdigest()
|
||||
|
||||
# The port MUST be 0.
|
||||
sock.connect((dest, 0))
|
||||
log.info('Socket connected.')
|
||||
|
||||
_close = sock.close
|
||||
def close(*args, **kwargs):
|
||||
with self._sessions_lock:
|
||||
if sid in self._sessions:
|
||||
del self._sessions[sid]
|
||||
_close()
|
||||
log.info('Socket closed.')
|
||||
sock.close = close
|
||||
|
||||
sock.peer_jid = peer
|
||||
sock.self_jid = target if requester == peer else requester
|
||||
|
||||
self.xmpp.event('socks_connected', sid)
|
||||
return sock
|
||||
factory = lambda: Socks5Protocol(dest, 0, self.xmpp.event)
|
||||
return self.xmpp.loop.create_connection(factory, proxy, proxy_port)
|
||||
|
||||
def _accept_stream(self, iq):
|
||||
receiver = iq['to']
|
||||
@@ -278,15 +267,13 @@ class XEP_0065(BasePlugin):
|
||||
return self.auto_accept
|
||||
|
||||
def _authorized_sid(self, jid, sid, ifrom, iq):
|
||||
with self._preauthed_sids_lock:
|
||||
log.debug('>>> authed sids: %s', self._preauthed_sids)
|
||||
log.debug('>>> lookup: %s %s %s', jid, sid, ifrom)
|
||||
if (jid, sid, ifrom) in self._preauthed_sids:
|
||||
del self._preauthed_sids[(jid, sid, ifrom)]
|
||||
return True
|
||||
return False
|
||||
log.debug('>>> authed sids: %s', self._preauthed_sids)
|
||||
log.debug('>>> lookup: %s %s %s', jid, sid, ifrom)
|
||||
if (jid, sid, ifrom) in self._preauthed_sids:
|
||||
del self._preauthed_sids[(jid, sid, ifrom)]
|
||||
return True
|
||||
return False
|
||||
|
||||
def _preauthorize_sid(self, jid, sid, ifrom, data):
|
||||
log.debug('>>>> %s %s %s %s', jid, sid, ifrom, data)
|
||||
with self._preauthed_sids_lock:
|
||||
self._preauthed_sids[(jid, sid, ifrom)] = True
|
||||
self._preauthed_sids[(jid, sid, ifrom)] = True
|
||||
|
||||
265
slixmpp/plugins/xep_0065/socks5.py
Normal file
265
slixmpp/plugins/xep_0065/socks5.py
Normal file
@@ -0,0 +1,265 @@
|
||||
'''Pure asyncio implementation of RFC 1928 - SOCKS Protocol Version 5.'''
|
||||
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from slixmpp.stringprep import punycode, StringprepError
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProtocolMismatch(Exception):
|
||||
'''We only implement SOCKS5, no other version or protocol.'''
|
||||
|
||||
|
||||
class ProtocolError(Exception):
|
||||
'''Some protocol error.'''
|
||||
|
||||
|
||||
class MethodMismatch(Exception):
|
||||
'''The server answered with a method we didn’t ask for.'''
|
||||
|
||||
|
||||
class MethodUnacceptable(Exception):
|
||||
'''None of our methods is supported by the server.'''
|
||||
|
||||
|
||||
class AddressTypeUnacceptable(Exception):
|
||||
'''The address type (ATYP) field isn’t one of IPv4, IPv6 or domain name.'''
|
||||
|
||||
|
||||
class ReplyError(Exception):
|
||||
'''The server answered with an error.'''
|
||||
|
||||
possible_values = (
|
||||
"succeeded",
|
||||
"general SOCKS server failure",
|
||||
"connection not allowed by ruleset",
|
||||
"Network unreachable",
|
||||
"Host unreachable",
|
||||
"Connection refused",
|
||||
"TTL expired",
|
||||
"Command not supported",
|
||||
"Address type not supported",
|
||||
"Unknown error")
|
||||
|
||||
def __init__(self, result):
|
||||
if result < 9:
|
||||
Exception.__init__(self, self.possible_values[result])
|
||||
else:
|
||||
Exception.__init__(self, self.possible_values[9])
|
||||
|
||||
|
||||
class Method(enum.IntEnum):
|
||||
'''Known methods for a SOCKS5 session.'''
|
||||
none = 0
|
||||
gssapi = 1
|
||||
password = 2
|
||||
# Methods 3 to 127 are reserved by IANA.
|
||||
# Methods 128 to 254 are reserved for private use.
|
||||
unacceptable = 255
|
||||
not_yet_selected = -1
|
||||
|
||||
|
||||
class Command(enum.IntEnum):
|
||||
'''Existing commands for requests.'''
|
||||
connect = 1
|
||||
bind = 2
|
||||
udp_associate = 3
|
||||
|
||||
|
||||
class AddressType(enum.IntEnum):
|
||||
'''Existing address types.'''
|
||||
ipv4 = 1
|
||||
domain = 3
|
||||
ipv6 = 4
|
||||
|
||||
|
||||
class Socks5Protocol(asyncio.Protocol):
|
||||
'''This implements SOCKS5 as an asyncio protocol.'''
|
||||
|
||||
def __init__(self, dest_addr, dest_port, event):
|
||||
self.methods = {Method.none}
|
||||
self.selected_method = Method.not_yet_selected
|
||||
self.transport = None
|
||||
self.dest = (dest_addr, dest_port)
|
||||
self.connected = asyncio.Future()
|
||||
self.event = event
|
||||
self.paused = asyncio.Future()
|
||||
self.paused.set_result(None)
|
||||
|
||||
def register_method(self, method):
|
||||
'''Register a SOCKS5 method.'''
|
||||
self.methods.add(method)
|
||||
|
||||
def unregister_method(self, method):
|
||||
'''Unregister a SOCKS5 method.'''
|
||||
self.methods.remove(method)
|
||||
|
||||
def connection_made(self, transport):
|
||||
'''Called when the connection to the SOCKS5 server is established.'''
|
||||
|
||||
log.debug('SOCKS5 connection established.')
|
||||
|
||||
self.transport = transport
|
||||
self._send_methods()
|
||||
|
||||
def data_received(self, data):
|
||||
'''Called when we received some data from the SOCKS5 server.'''
|
||||
|
||||
log.debug('SOCKS5 message received.')
|
||||
|
||||
# If we are already connected, this is a data packet.
|
||||
if self.connected.done():
|
||||
return self.event('socks5_data', data)
|
||||
|
||||
# Every SOCKS5 message starts with the protocol version.
|
||||
if data[0] != 5:
|
||||
raise ProtocolMismatch()
|
||||
|
||||
# Then select the correct handler for the data we just received.
|
||||
if self.selected_method == Method.not_yet_selected:
|
||||
self._handle_method(data)
|
||||
else:
|
||||
self._handle_connect(data)
|
||||
|
||||
def connection_lost(self, exc):
|
||||
log.debug('SOCKS5 connection closed.')
|
||||
self.event('socks5_closed', exc)
|
||||
|
||||
def pause_writing(self):
|
||||
self.paused = asyncio.Future()
|
||||
|
||||
def resume_writing(self):
|
||||
self.paused.set_result(None)
|
||||
|
||||
def write(self, data):
|
||||
yield from self.paused
|
||||
self.transport.write(data)
|
||||
|
||||
def _send_methods(self):
|
||||
'''Send the methods request, first thing a client should do.'''
|
||||
|
||||
# Create the buffer for our request.
|
||||
request = bytearray(len(self.methods) + 2)
|
||||
|
||||
# Protocol version.
|
||||
request[0] = 5
|
||||
|
||||
# Number of methods to send.
|
||||
request[1] = len(self.methods)
|
||||
|
||||
# List every method we support.
|
||||
for i, method in enumerate(self.methods):
|
||||
request[i + 2] = method
|
||||
|
||||
# Send the request.
|
||||
self.transport.write(request)
|
||||
|
||||
def _send_request(self, command):
|
||||
'''Send a request, should be done after having negociated a method.'''
|
||||
|
||||
# Encode the destination address to embed it in our request.
|
||||
# We need to do that first because its length is variable.
|
||||
address, port = self.dest
|
||||
addr = self._encode_addr(address)
|
||||
|
||||
# Create the buffer for our request.
|
||||
request = bytearray(5 + len(addr))
|
||||
|
||||
# Protocol version.
|
||||
request[0] = 5
|
||||
|
||||
# Specify the command we want to use.
|
||||
request[1] = command
|
||||
|
||||
# request[2] is reserved, keeping it at 0.
|
||||
|
||||
# Add our destination address and port.
|
||||
request[3:3+len(addr)] = addr
|
||||
request[-2:] = struct.pack('>H', port)
|
||||
|
||||
# Send the request.
|
||||
log.debug('SOCKS5 message sent.')
|
||||
self.transport.write(request)
|
||||
|
||||
def _handle_method(self, data):
|
||||
'''Handle a method reply from the server.'''
|
||||
|
||||
if len(data) != 2:
|
||||
raise ProtocolError()
|
||||
selected_method = data[1]
|
||||
if selected_method not in self.methods:
|
||||
raise MethodMismatch()
|
||||
if selected_method == Method.unacceptable:
|
||||
raise MethodUnacceptable()
|
||||
self.selected_method = selected_method
|
||||
self._send_request(Command.connect)
|
||||
|
||||
def _handle_connect(self, data):
|
||||
'''Handle a connect reply from the server.'''
|
||||
|
||||
try:
|
||||
addr, port = self._parse_result(data)
|
||||
except ReplyError as exception:
|
||||
self.connected.set_exception(exception)
|
||||
self.connected.set_result((addr, port))
|
||||
self.event('socks5_connected', (addr, port))
|
||||
|
||||
def _parse_result(self, data):
|
||||
'''Parse a reply from the server.'''
|
||||
|
||||
result = data[1]
|
||||
if result != 0:
|
||||
raise ReplyError(result)
|
||||
addr = self._parse_addr(data[3:-2])
|
||||
port = struct.unpack('>H', data[-2:])[0]
|
||||
return (addr, port)
|
||||
|
||||
@staticmethod
|
||||
def _parse_addr(addr):
|
||||
'''Parse an address (IP or domain) from a bytestream.'''
|
||||
|
||||
addr_type = addr[0]
|
||||
if addr_type == AddressType.ipv6:
|
||||
try:
|
||||
return socket.inet_ntop(socket.AF_INET6, addr[1:])
|
||||
except ValueError as e:
|
||||
raise AddressTypeUnacceptable(e)
|
||||
if addr_type == AddressType.ipv4:
|
||||
try:
|
||||
return socket.inet_ntop(socket.AF_INET, addr[1:])
|
||||
except ValueError as e:
|
||||
raise AddressTypeUnacceptable(e)
|
||||
if addr_type == AddressType.domain:
|
||||
length = addr[1]
|
||||
address = addr[2:]
|
||||
if length != len(address):
|
||||
raise Exception('Size mismatch')
|
||||
return address.decode()
|
||||
raise AddressTypeUnacceptable(addr_type)
|
||||
|
||||
@staticmethod
|
||||
def _encode_addr(addr):
|
||||
'''Encode an address (IP or domain) into a bytestream.'''
|
||||
|
||||
try:
|
||||
ipv6 = socket.inet_pton(socket.AF_INET6, addr)
|
||||
return b'\x04' + ipv6
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
ipv4 = socket.inet_aton(addr)
|
||||
return b'\x01' + ipv4
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
domain = punycode(addr)
|
||||
return b'\x03' + bytes([len(domain)]) + domain
|
||||
except StringprepError:
|
||||
pass
|
||||
raise Exception('Err…')
|
||||
@@ -81,26 +81,25 @@ class XEP_0077(BasePlugin):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_registration(self, jid=None, ifrom=None, block=True,
|
||||
def get_registration(self, jid=None, ifrom=None,
|
||||
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)
|
||||
return iq.send(timeout=timeout, callback=callback)
|
||||
|
||||
def cancel_registration(self, jid=None, ifrom=None, block=True,
|
||||
def cancel_registration(self, jid=None, ifrom=None,
|
||||
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)
|
||||
return iq.send(timeout=timeout, callback=callback)
|
||||
|
||||
def change_password(self, password, jid=None, ifrom=None, block=True,
|
||||
def change_password(self, password, jid=None, ifrom=None,
|
||||
timeout=None, callback=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
@@ -112,4 +111,4 @@ class XEP_0077(BasePlugin):
|
||||
else:
|
||||
iq['register']['username'] = self.xmpp.boundjid.user
|
||||
iq['register']['password'] = password
|
||||
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||
return iq.send(timeout=timeout, callback=callback)
|
||||
|
||||
@@ -128,9 +128,8 @@ class XEP_0078(BasePlugin):
|
||||
|
||||
self.xmpp.authenticated = True
|
||||
|
||||
self.xmpp.boundjid = JID(self.xmpp.requested_jid,
|
||||
resource=resource,
|
||||
cache_lock=True)
|
||||
self.xmpp.boundjid = JID(self.xmpp.requested_jid)
|
||||
self.xmpp.boundjid.resource = resource
|
||||
self.xmpp.event('session_bind', self.xmpp.boundjid)
|
||||
|
||||
log.debug("Established Session")
|
||||
|
||||
@@ -76,8 +76,6 @@ class XEP_0080(BasePlugin):
|
||||
|
||||
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 slixmpp.xmlstream.RESPONSE_TIMEOUT
|
||||
@@ -86,10 +84,10 @@ class XEP_0080(BasePlugin):
|
||||
"""
|
||||
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'):
|
||||
timeout_callback = kwargs.get('timeout_callback', None)
|
||||
for param in ('ifrom', 'block', 'callback', 'timeout', 'options', 'timeout_callback'):
|
||||
if param in kwargs:
|
||||
del kwargs[param]
|
||||
|
||||
@@ -99,18 +97,16 @@ class XEP_0080(BasePlugin):
|
||||
return self.xmpp['xep_0163'].publish(geoloc,
|
||||
options=options,
|
||||
ifrom=ifrom,
|
||||
block=block,
|
||||
callback=callback,
|
||||
timeout=timeout)
|
||||
timeout=timeout,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
def stop(self, ifrom=None, block=True, callback=None, timeout=None):
|
||||
def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=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 slixmpp.xmlstream.RESPONSE_TIMEOUT
|
||||
@@ -120,6 +116,6 @@ class XEP_0080(BasePlugin):
|
||||
geoloc = Geoloc()
|
||||
return self.xmpp['xep_0163'].publish(geoloc,
|
||||
ifrom=ifrom,
|
||||
block=block,
|
||||
callback=callback,
|
||||
timeout=timeout)
|
||||
timeout=timeout,
|
||||
timeout_callback=None)
|
||||
|
||||
@@ -44,28 +44,29 @@ class XEP_0084(BasePlugin):
|
||||
def generate_id(self, data):
|
||||
return hashlib.sha1(data).hexdigest()
|
||||
|
||||
def retrieve_avatar(self, jid, id, url=None, ifrom=None, block=True,
|
||||
callback=None, timeout=None):
|
||||
def retrieve_avatar(self, jid, id, url=None, ifrom=None,
|
||||
callback=None, timeout=None, timeout_callback=None):
|
||||
return self.xmpp['xep_0060'].get_item(jid, Data.namespace, id,
|
||||
ifrom=ifrom,
|
||||
block=block,
|
||||
callback=callback,
|
||||
timeout=timeout)
|
||||
timeout=timeout,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
def publish_avatar(self, data, ifrom=None, block=True, callback=None,
|
||||
timeout=None):
|
||||
def publish_avatar(self, data, ifrom=None, callback=None,
|
||||
timeout=None, timeout_callback=None):
|
||||
payload = Data()
|
||||
payload['value'] = data
|
||||
return self.xmpp['xep_0163'].publish(payload,
|
||||
id=self.generate_id(data),
|
||||
ifrom=ifrom,
|
||||
block=block,
|
||||
callback=callback,
|
||||
timeout=timeout)
|
||||
timeout=timeout,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
def publish_avatar_metadata(self, items=None, pointers=None,
|
||||
ifrom=None, block=True,
|
||||
callback=None, timeout=None):
|
||||
ifrom=None,
|
||||
callback=None, timeout=None,
|
||||
timeout_callback=None):
|
||||
metadata = MetaData()
|
||||
if items is None:
|
||||
items = []
|
||||
@@ -84,18 +85,16 @@ class XEP_0084(BasePlugin):
|
||||
return self.xmpp['xep_0163'].publish(metadata,
|
||||
id=info['id'],
|
||||
ifrom=ifrom,
|
||||
block=block,
|
||||
callback=callback,
|
||||
timeout=timeout)
|
||||
timeout=timeout,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
def stop(self, ifrom=None, block=True, callback=None, timeout=None):
|
||||
def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None):
|
||||
"""
|
||||
Clear existing avatar metadata 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 slixmpp.xmlstream.RESPONSE_TIMEOUT
|
||||
@@ -106,6 +105,6 @@ class XEP_0084(BasePlugin):
|
||||
return self.xmpp['xep_0163'].publish(metadata,
|
||||
node=MetaData.namespace,
|
||||
ifrom=ifrom,
|
||||
block=block,
|
||||
callback=callback,
|
||||
timeout=timeout)
|
||||
timeout=timeout,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
@@ -21,11 +21,11 @@ class Data(ElementBase):
|
||||
def get_value(self):
|
||||
if self.xml.text:
|
||||
return b64decode(bytes(self.xml.text))
|
||||
return ''
|
||||
return b''
|
||||
|
||||
def set_value(self, value):
|
||||
if value:
|
||||
self.xml.text = b64encode(bytes(value))
|
||||
self.xml.text = b64encode(bytes(value)).decode()
|
||||
else:
|
||||
self.xml.text = ''
|
||||
|
||||
|
||||
@@ -64,13 +64,14 @@ class XEP_0092(BasePlugin):
|
||||
Arguments:
|
||||
iq -- The Iq stanza containing the software version query.
|
||||
"""
|
||||
iq.reply()
|
||||
iq = iq.reply()
|
||||
iq['software_version']['name'] = self.software_name
|
||||
iq['software_version']['version'] = self.version
|
||||
iq['software_version']['os'] = self.os
|
||||
iq.send()
|
||||
|
||||
def get_version(self, jid, ifrom=None, block=True, timeout=None, callback=None):
|
||||
def get_version(self, jid, ifrom=None, timeout=None, callback=None,
|
||||
timeout_callback=None):
|
||||
"""
|
||||
Retrieve the software version of a remote agent.
|
||||
|
||||
@@ -82,4 +83,5 @@ class XEP_0092(BasePlugin):
|
||||
iq['from'] = ifrom
|
||||
iq['type'] = 'get'
|
||||
iq['query'] = Version.namespace
|
||||
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||
return iq.send(timeout=timeout, callback=callback,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
@@ -182,7 +182,6 @@ class XEP_0095(BasePlugin):
|
||||
if stream_handler:
|
||||
self.xmpp.add_event_handler('stream:%s:%s' % (sid, jid),
|
||||
stream_handler,
|
||||
threaded=True,
|
||||
disposable=True)
|
||||
return iq.send()
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class XEP_0096(BasePlugin):
|
||||
data['size'] = size
|
||||
data['date'] = date
|
||||
data['desc'] = desc
|
||||
data['hash'] = hash
|
||||
if allow_ranged:
|
||||
data.enable('range')
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from slixmpp.stanza import StreamFeatures, Presence, Iq
|
||||
from slixmpp.xmlstream import register_stanza_plugin, JID
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.xmlstream.matcher import StanzaPath
|
||||
from slixmpp import asyncio
|
||||
from slixmpp.exceptions import XMPPError, IqError, IqTimeout
|
||||
from slixmpp.plugins import BasePlugin
|
||||
from slixmpp.plugins.xep_0115 import stanza, StaticCaps
|
||||
@@ -131,6 +132,7 @@ class XEP_0115(BasePlugin):
|
||||
|
||||
self.xmpp.event('entity_caps', p)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _process_caps(self, pres):
|
||||
if not pres['caps']['hash']:
|
||||
log.debug("Received unsupported legacy caps: %s, %s, %s",
|
||||
@@ -162,7 +164,8 @@ class XEP_0115(BasePlugin):
|
||||
log.debug("New caps verification string: %s", ver)
|
||||
try:
|
||||
node = '%s#%s' % (pres['caps']['node'], ver)
|
||||
caps = self.xmpp['xep_0030'].get_info(pres['from'], node)
|
||||
caps = yield from self.xmpp['xep_0030'].get_info(pres['from'], node,
|
||||
coroutine=True)
|
||||
|
||||
if isinstance(caps, Iq):
|
||||
caps = caps['disco_info']
|
||||
@@ -277,9 +280,10 @@ class XEP_0115(BasePlugin):
|
||||
binary = hash(S.encode('utf8')).digest()
|
||||
return base64.b64encode(binary).decode('utf-8')
|
||||
|
||||
@asyncio.coroutine
|
||||
def update_caps(self, jid=None, node=None, preserve=False):
|
||||
try:
|
||||
info = self.xmpp['xep_0030'].get_info(jid, node, local=True)
|
||||
info = yield from 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)
|
||||
|
||||
@@ -35,7 +35,7 @@ class XEP_0118(BasePlugin):
|
||||
|
||||
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):
|
||||
ifrom=None, callback=None, timeout=None, timeout_callback=None):
|
||||
"""
|
||||
Publish the user's current tune.
|
||||
|
||||
@@ -49,8 +49,6 @@ class XEP_0118(BasePlugin):
|
||||
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 slixmpp.xmlstream.RESPONSE_TIMEOUT
|
||||
@@ -69,18 +67,16 @@ class XEP_0118(BasePlugin):
|
||||
node=UserTune.namespace,
|
||||
options=options,
|
||||
ifrom=ifrom,
|
||||
block=block,
|
||||
callback=callback,
|
||||
timeout=timeout)
|
||||
timeout=timeout,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
def stop(self, ifrom=None, block=True, callback=None, timeout=None):
|
||||
def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=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 slixmpp.xmlstream.RESPONSE_TIMEOUT
|
||||
@@ -91,6 +87,6 @@ class XEP_0118(BasePlugin):
|
||||
return self.xmpp['xep_0163'].publish(tune,
|
||||
node=UserTune.namespace,
|
||||
ifrom=ifrom,
|
||||
block=block,
|
||||
callback=callback,
|
||||
timeout=timeout)
|
||||
timeout=timeout,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
11
slixmpp/plugins/xep_0122/__init__.py
Normal file
11
slixmpp/plugins/xep_0122/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
from slixmpp.plugins.base import register_plugin
|
||||
from slixmpp.plugins.xep_0122.stanza import FormValidation
|
||||
from slixmpp.plugins.xep_0122.data_validation import XEP_0122
|
||||
|
||||
|
||||
register_plugin(XEP_0122)
|
||||
|
||||
|
||||
# Retain some backwards compatibility
|
||||
xep_0122 = XEP_0122
|
||||
19
slixmpp/plugins/xep_0122/data_validation.py
Normal file
19
slixmpp/plugins/xep_0122/data_validation.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
from slixmpp.plugins import BasePlugin
|
||||
from slixmpp.plugins.xep_0004 import stanza
|
||||
from slixmpp.plugins.xep_0004.stanza import FormField
|
||||
from slixmpp.plugins.xep_0122.stanza import FormValidation
|
||||
|
||||
|
||||
class XEP_0122(BasePlugin):
|
||||
"""
|
||||
XEP-0122: Data Forms
|
||||
"""
|
||||
|
||||
name = 'xep_0122'
|
||||
description = 'XEP-0122: Data Forms Validation'
|
||||
dependencies = set(['xep_0004'])
|
||||
stanza = stanza
|
||||
|
||||
def plugin_init(self):
|
||||
register_stanza_plugin(FormField, FormValidation)
|
||||
93
slixmpp/plugins/xep_0122/stanza.py
Normal file
93
slixmpp/plugins/xep_0122/stanza.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from slixmpp.xmlstream import ElementBase, ET
|
||||
|
||||
|
||||
class FormValidation(ElementBase):
|
||||
"""
|
||||
Validation values for form fields.
|
||||
|
||||
Example:
|
||||
|
||||
<field var='evt.date' type='text-single' label='Event Date/Time'>
|
||||
<validate xmlns='http://jabber.org/protocol/xdata-validate'
|
||||
datatype='xs:dateTime'/>
|
||||
<value>2003-10-06T11:22:00-07:00</value>
|
||||
</field>
|
||||
|
||||
Questions:
|
||||
Should this look at the datatype value and convert the range values as appropriate?
|
||||
Should this stanza provide a pass/fail for a value from the field, or convert field value to datatype?
|
||||
"""
|
||||
|
||||
namespace = 'http://jabber.org/protocol/xdata-validate'
|
||||
name = 'validate'
|
||||
plugin_attrib = 'validate'
|
||||
interfaces = {'datatype', 'basic', 'open', 'range', 'regex', }
|
||||
sub_interfaces = {'basic', 'open', 'range', 'regex', }
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
def _add_field(self, name):
|
||||
self.remove_all()
|
||||
item_xml = ET.Element('{%s}%s' % (self.namespace, name))
|
||||
self.xml.append(item_xml)
|
||||
return item_xml
|
||||
|
||||
def set_basic(self, value):
|
||||
if value:
|
||||
self._add_field('basic')
|
||||
else:
|
||||
del self['basic']
|
||||
|
||||
def set_open(self, value):
|
||||
if value:
|
||||
self._add_field('open')
|
||||
else:
|
||||
del self['open']
|
||||
|
||||
def set_regex(self, regex):
|
||||
if regex:
|
||||
_regex = self._add_field('regex')
|
||||
_regex.text = regex
|
||||
else:
|
||||
del self['regex']
|
||||
|
||||
def set_range(self, value, minimum=None, maximum=None):
|
||||
if value:
|
||||
_range = self._add_field('range')
|
||||
_range.attrib['min'] = str(minimum)
|
||||
_range.attrib['max'] = str(maximum)
|
||||
else:
|
||||
del self['range']
|
||||
|
||||
def remove_all(self, except_tag=None):
|
||||
for a in self.sub_interfaces:
|
||||
if a != except_tag:
|
||||
del self[a]
|
||||
|
||||
def get_basic(self):
|
||||
present = self.xml.find('{%s}basic' % self.namespace)
|
||||
return present is not None
|
||||
|
||||
def get_open(self):
|
||||
present = self.xml.find('{%s}open' % self.namespace)
|
||||
return present is not None
|
||||
|
||||
def get_regex(self):
|
||||
present = self.xml.find('{%s}regex' % self.namespace)
|
||||
if present is not None:
|
||||
return present.text
|
||||
|
||||
return False
|
||||
|
||||
def get_range(self):
|
||||
present = self.xml.find('{%s}range' % self.namespace)
|
||||
if present is not None:
|
||||
attributes = present.attrib
|
||||
return_value = dict()
|
||||
if 'min' in attributes:
|
||||
return_value['minimum'] = attributes['min']
|
||||
if 'max' in attributes:
|
||||
return_value['maximum'] = attributes['max']
|
||||
return return_value
|
||||
|
||||
return False
|
||||
@@ -35,15 +35,14 @@ class XEP_0133(BasePlugin):
|
||||
|
||||
|
||||
def create_command(name):
|
||||
def admin_command(self, jid=None, session=None, ifrom=None, block=False):
|
||||
def admin_command(self, jid=None, session=None, ifrom=None):
|
||||
if jid is None:
|
||||
jid = self.xmpp.boundjid.server
|
||||
self.xmpp['xep_0050'].start_command(
|
||||
jid=jid,
|
||||
node='http://jabber.org/protocol/admin#%s' % name,
|
||||
session=session,
|
||||
ifrom=ifrom,
|
||||
block=block)
|
||||
ifrom=ifrom)
|
||||
return admin_command
|
||||
|
||||
|
||||
|
||||
145
slixmpp/plugins/xep_0138.py
Normal file
145
slixmpp/plugins/xep_0138.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz
|
||||
This file is part of slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import zlib
|
||||
|
||||
|
||||
from slixmpp.stanza import StreamFeatures
|
||||
from slixmpp.xmlstream import RestartStream, register_stanza_plugin, ElementBase, StanzaBase
|
||||
from slixmpp.xmlstream.matcher import *
|
||||
from slixmpp.xmlstream.handler import *
|
||||
from slixmpp.plugins import BasePlugin, register_plugin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Compression(ElementBase):
|
||||
name = 'compression'
|
||||
namespace = 'http://jabber.org/features/compress'
|
||||
interfaces = set(('methods',))
|
||||
plugin_attrib = 'compression'
|
||||
plugin_tag_map = {}
|
||||
plugin_attrib_map = {}
|
||||
|
||||
def get_methods(self):
|
||||
methods = []
|
||||
for method in self.xml.findall('{%s}method' % self.namespace):
|
||||
methods.append(method.text)
|
||||
return methods
|
||||
|
||||
|
||||
class Compress(StanzaBase):
|
||||
name = 'compress'
|
||||
namespace = 'http://jabber.org/protocol/compress'
|
||||
interfaces = set(('method',))
|
||||
sub_interfaces = interfaces
|
||||
plugin_attrib = 'compress'
|
||||
plugin_tag_map = {}
|
||||
plugin_attrib_map = {}
|
||||
|
||||
def setup(self, xml):
|
||||
StanzaBase.setup(self, xml)
|
||||
self.xml.tag = self.tag_name()
|
||||
|
||||
|
||||
class Compressed(StanzaBase):
|
||||
name = 'compressed'
|
||||
namespace = 'http://jabber.org/protocol/compress'
|
||||
interfaces = set()
|
||||
plugin_tag_map = {}
|
||||
plugin_attrib_map = {}
|
||||
|
||||
def setup(self, xml):
|
||||
StanzaBase.setup(self, xml)
|
||||
self.xml.tag = self.tag_name()
|
||||
|
||||
|
||||
|
||||
|
||||
class ZlibSocket(object):
|
||||
|
||||
def __init__(self, socketobj):
|
||||
self.__socket = socketobj
|
||||
self.compressor = zlib.compressobj()
|
||||
self.decompressor = zlib.decompressobj(zlib.MAX_WBITS)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.__socket, name)
|
||||
|
||||
def send(self, data):
|
||||
sentlen = len(data)
|
||||
data = self.compressor.compress(data)
|
||||
data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
|
||||
log.debug(b'>>> (compressed)' + (data.encode("hex")))
|
||||
#return self.__socket.send(data)
|
||||
sentactuallen = self.__socket.send(data)
|
||||
assert(sentactuallen == len(data))
|
||||
|
||||
return sentlen
|
||||
|
||||
def recv(self, *args, **kwargs):
|
||||
data = self.__socket.recv(*args, **kwargs)
|
||||
log.debug(b'<<< (compressed)' + data.encode("hex"))
|
||||
return self.decompressor.decompress(self.decompressor.unconsumed_tail + data)
|
||||
|
||||
|
||||
class XEP_0138(BasePlugin):
|
||||
"""
|
||||
XEP-0138: Compression
|
||||
"""
|
||||
name = "xep_0138"
|
||||
description = "XEP-0138: Compression"
|
||||
dependencies = set(["xep_0030"])
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0138'
|
||||
self.description = 'Stream Compression (Generic)'
|
||||
|
||||
self.compression_methods = {'zlib': True}
|
||||
|
||||
register_stanza_plugin(StreamFeatures, Compression)
|
||||
self.xmpp.register_stanza(Compress)
|
||||
self.xmpp.register_stanza(Compressed)
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('Compressed',
|
||||
StanzaPath('compressed'),
|
||||
self._handle_compressed,
|
||||
instream=True))
|
||||
|
||||
self.xmpp.register_feature('compression',
|
||||
self._handle_compression,
|
||||
restart=True,
|
||||
order=self.config.get('order', 5))
|
||||
|
||||
def register_compression_method(self, name, handler):
|
||||
self.compression_methods[name] = handler
|
||||
|
||||
def _handle_compression(self, features):
|
||||
for method in features['compression']['methods']:
|
||||
if method in self.compression_methods:
|
||||
log.info('Attempting to use %s compression' % method)
|
||||
c = Compress(self.xmpp)
|
||||
c['method'] = method
|
||||
c.send(now=True)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _handle_compressed(self, stanza):
|
||||
self.xmpp.features.add('compression')
|
||||
log.debug('Stream Compressed!')
|
||||
compressed_socket = ZlibSocket(self.xmpp.socket)
|
||||
self.xmpp.set_socket(compressed_socket)
|
||||
raise RestartStream()
|
||||
|
||||
def _handle_failure(self, stanza):
|
||||
pass
|
||||
|
||||
xep_0138 = XEP_0138
|
||||
register_plugin(XEP_0138)
|
||||
@@ -33,8 +33,9 @@ class XEP_0152(BasePlugin):
|
||||
def session_bind(self, jid):
|
||||
self.xmpp['xep_0163'].register_pep('reachability', Reachability)
|
||||
|
||||
def publish_reachability(self, addresses, options=None,
|
||||
ifrom=None, block=True, callback=None, timeout=None):
|
||||
def publish_reachability(self, addresses, options=None, ifrom=None,
|
||||
callback=None, timeout=None,
|
||||
timeout_callback=None):
|
||||
"""
|
||||
Publish alternative addresses where the user can be reached.
|
||||
|
||||
@@ -43,8 +44,6 @@ class XEP_0152(BasePlugin):
|
||||
optional description for each address.
|
||||
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 slixmpp.xmlstream.RESPONSE_TIMEOUT
|
||||
@@ -66,18 +65,16 @@ class XEP_0152(BasePlugin):
|
||||
node=Reachability.namespace,
|
||||
options=options,
|
||||
ifrom=ifrom,
|
||||
block=block,
|
||||
callback=callback,
|
||||
timeout=timeout)
|
||||
timeout=timeout,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
def stop(self, ifrom=None, block=True, callback=None, timeout=None):
|
||||
def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=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 slixmpp.xmlstream.RESPONSE_TIMEOUT
|
||||
@@ -88,6 +85,6 @@ class XEP_0152(BasePlugin):
|
||||
return self.xmpp['xep_0163'].publish(reach,
|
||||
node=Reachability.namespace,
|
||||
ifrom=ifrom,
|
||||
block=block,
|
||||
callback=callback,
|
||||
timeout=timeout)
|
||||
timeout=timeout,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from slixmpp.stanza import Presence
|
||||
from slixmpp.exceptions import XMPPError
|
||||
from slixmpp.exceptions import XMPPError, IqTimeout
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
from slixmpp.plugins.base import BasePlugin
|
||||
from slixmpp.plugins.xep_0153 import stanza, VCardTempUpdate
|
||||
from slixmpp import asyncio, future_wrapper
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -30,8 +30,6 @@ class XEP_0153(BasePlugin):
|
||||
def plugin_init(self):
|
||||
self._hashes = {}
|
||||
|
||||
self._allow_advertising = threading.Event()
|
||||
|
||||
register_stanza_plugin(Presence, VCardTempUpdate)
|
||||
|
||||
self.xmpp.add_filter('out', self._update_presence)
|
||||
@@ -59,36 +57,62 @@ class XEP_0153(BasePlugin):
|
||||
self.xmpp.del_event_handler('presence_chat', self._recv_presence)
|
||||
self.xmpp.del_event_handler('presence_away', self._recv_presence)
|
||||
|
||||
def set_avatar(self, jid=None, avatar=None, mtype=None, block=True,
|
||||
timeout=None, callback=None):
|
||||
@future_wrapper
|
||||
def set_avatar(self, jid=None, avatar=None, mtype=None, timeout=None,
|
||||
callback=None, timeout_callback=None):
|
||||
if jid is None:
|
||||
jid = self.xmpp.boundjid.bare
|
||||
|
||||
vcard = self.xmpp['xep_0054'].get_vcard(jid, cached=True)
|
||||
vcard = vcard['vcard_temp']
|
||||
vcard['PHOTO']['TYPE'] = mtype
|
||||
vcard['PHOTO']['BINVAL'] = avatar
|
||||
future = asyncio.Future()
|
||||
|
||||
self.xmpp['xep_0054'].publish_vcard(jid=jid, vcard=vcard)
|
||||
def propagate_timeout_exception(fut):
|
||||
try:
|
||||
fut.done()
|
||||
except IqTimeout as e:
|
||||
future.set_exception(e)
|
||||
|
||||
self.api['reset_hash'](jid)
|
||||
self.xmpp.roster[jid].send_last_presence()
|
||||
def custom_callback(result):
|
||||
vcard = result['vcard_temp']
|
||||
vcard['PHOTO']['TYPE'] = mtype
|
||||
vcard['PHOTO']['BINVAL'] = avatar
|
||||
|
||||
new_future = self.xmpp['xep_0054'].publish_vcard(jid=jid,
|
||||
vcard=vcard,
|
||||
timeout=timeout,
|
||||
callback=next_callback,
|
||||
timeout_callback=timeout_callback)
|
||||
new_future.add_done_callback(propagate_timeout_exception)
|
||||
|
||||
def next_callback(result):
|
||||
if result['type'] == 'error':
|
||||
future.set_exception(result)
|
||||
else:
|
||||
self.api['reset_hash'](jid)
|
||||
self.xmpp.roster[jid].send_last_presence()
|
||||
|
||||
future.set_result(result)
|
||||
|
||||
first_future = self.xmpp['xep_0054'].get_vcard(jid, cached=False, timeout=timeout,
|
||||
callback=custom_callback,
|
||||
timeout_callback=timeout_callback)
|
||||
first_future.add_done_callback(propagate_timeout_exception)
|
||||
return future
|
||||
|
||||
@asyncio.coroutine
|
||||
def _start(self, event):
|
||||
try:
|
||||
vcard = self.xmpp['xep_0054'].get_vcard(self.xmpp.boundjid.bare)
|
||||
vcard = yield from self.xmpp['xep_0054'].get_vcard(self.xmpp.boundjid.bare)
|
||||
data = vcard['vcard_temp']['PHOTO']['BINVAL']
|
||||
if not data:
|
||||
new_hash = ''
|
||||
else:
|
||||
new_hash = hashlib.sha1(data).hexdigest()
|
||||
self.api['set_hash'](self.xmpp.boundjid, args=new_hash)
|
||||
self._allow_advertising.set()
|
||||
except XMPPError:
|
||||
log.debug('Could not retrieve vCard for %s' % self.xmpp.boundjid.bare)
|
||||
log.debug('Could not retrieve vCard for %s', self.xmpp.boundjid.bare)
|
||||
|
||||
def _end(self, event):
|
||||
self._allow_advertising.clear()
|
||||
pass
|
||||
|
||||
def _update_presence(self, stanza):
|
||||
if not isinstance(stanza, Presence):
|
||||
@@ -110,9 +134,10 @@ class XEP_0153(BasePlugin):
|
||||
if own_jid:
|
||||
self.xmpp.roster[jid].send_last_presence()
|
||||
|
||||
try:
|
||||
iq = self.xmpp['xep_0054'].get_vcard(jid=jid.bare, ifrom=ifrom)
|
||||
|
||||
def callback(iq):
|
||||
if iq['type'] == 'error':
|
||||
log.debug('Could not retrieve vCard for %s', jid)
|
||||
return
|
||||
data = iq['vcard_temp']['PHOTO']['BINVAL']
|
||||
if not data:
|
||||
new_hash = ''
|
||||
@@ -120,8 +145,9 @@ class XEP_0153(BasePlugin):
|
||||
new_hash = hashlib.sha1(data).hexdigest()
|
||||
|
||||
self.api['set_hash'](jid, args=new_hash)
|
||||
except XMPPError:
|
||||
log.debug('Could not retrieve vCard for %s' % jid)
|
||||
|
||||
self.xmpp['xep_0054'].get_vcard(jid=jid.bare, ifrom=ifrom,
|
||||
callback=callback)
|
||||
|
||||
def _recv_presence(self, pres):
|
||||
try:
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from slixmpp import asyncio
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
from slixmpp.plugins.base import BasePlugin, register_plugin
|
||||
|
||||
@@ -61,7 +62,7 @@ class XEP_0163(BasePlugin):
|
||||
for ns in namespace:
|
||||
self.xmpp['xep_0030'].add_feature('%s+notify' % ns,
|
||||
jid=jid)
|
||||
self.xmpp['xep_0115'].update_caps(jid)
|
||||
asyncio.async(self.xmpp['xep_0115'].update_caps(jid))
|
||||
|
||||
def remove_interest(self, namespace, jid=None):
|
||||
"""
|
||||
@@ -80,7 +81,7 @@ class XEP_0163(BasePlugin):
|
||||
for ns in namespace:
|
||||
self.xmpp['xep_0030'].del_feature(jid=jid,
|
||||
feature='%s+notify' % namespace)
|
||||
self.xmpp['xep_0115'].update_caps(jid)
|
||||
asyncio.async(self.xmpp['xep_0115'].update_caps(jid))
|
||||
|
||||
def publish(self, stanza, node=None, id=None, options=None, ifrom=None,
|
||||
timeout_callback=None, callback=None, timeout=None):
|
||||
|
||||
@@ -67,9 +67,7 @@ class XEP_0184(BasePlugin):
|
||||
"""
|
||||
ack = self.xmpp.Message()
|
||||
ack['to'] = msg['from']
|
||||
ack['from'] = msg['to']
|
||||
ack['receipt'] = msg['id']
|
||||
ack['id'] = msg['id']
|
||||
ack.send()
|
||||
|
||||
def _handle_receipt_received(self, msg):
|
||||
|
||||
@@ -27,18 +27,18 @@ class XEP_0186(BasePlugin):
|
||||
register_stanza_plugin(Iq, Visible)
|
||||
register_stanza_plugin(Iq, Invisible)
|
||||
|
||||
def set_invisible(self, ifrom=None, block=True, callback=None,
|
||||
def set_invisible(self, ifrom=None, callback=None,
|
||||
timeout=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq['from'] = ifrom
|
||||
iq.enable('invisible')
|
||||
iq.send(block=block, callback=callback, timeout=timeout)
|
||||
iq.send(callback=callback, timeout=timeout)
|
||||
|
||||
def set_visible(self, ifrom=None, block=True, callback=None,
|
||||
def set_visible(self, ifrom=None, callback=None,
|
||||
timeout=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq['from'] = ifrom
|
||||
iq.enable('visible')
|
||||
iq.send(block=block, callback=callback, timeout=timeout)
|
||||
iq.send(callback=callback, timeout=timeout)
|
||||
|
||||
@@ -45,14 +45,17 @@ class XEP_0191(BasePlugin):
|
||||
self.xmpp.remove_handler('Blocked Contact')
|
||||
self.xmpp.remove_handler('Unblocked Contact')
|
||||
|
||||
def get_blocked(self, ifrom=None, block=True, timeout=None, callback=None):
|
||||
def get_blocked(self, ifrom=None, timeout=None, callback=None,
|
||||
timeout_callback=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['from'] = ifrom
|
||||
iq.enable('blocklist')
|
||||
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||
return iq.send(timeout=timeout, callback=callback,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
def block(self, jids, ifrom=None, block=True, timeout=None, callback=None):
|
||||
def block(self, jids, ifrom=None, timeout=None, callback=None,
|
||||
timeout_callback=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq['from'] = ifrom
|
||||
@@ -61,9 +64,11 @@ class XEP_0191(BasePlugin):
|
||||
jids = [jids]
|
||||
|
||||
iq['block']['items'] = jids
|
||||
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||
return iq.send(timeout=timeout, callback=callback,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
def unblock(self, jids=None, ifrom=None, block=True, timeout=None, callback=None):
|
||||
def unblock(self, jids=None, ifrom=None, timeout=None, callback=None,
|
||||
timeout_callback=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq['from'] = ifrom
|
||||
@@ -74,7 +79,8 @@ class XEP_0191(BasePlugin):
|
||||
jids = [jids]
|
||||
|
||||
iq['unblock']['items'] = jids
|
||||
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||
return iq.send(timeout=timeout, callback=callback,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
def _handle_blocked(self, iq):
|
||||
self.xmpp.event('blocked', iq)
|
||||
|
||||
@@ -11,6 +11,7 @@ import logging
|
||||
|
||||
from slixmpp.jid import JID
|
||||
from slixmpp.stanza import Iq
|
||||
from slixmpp import asyncio
|
||||
from slixmpp.exceptions import IqError, IqTimeout
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
from slixmpp.xmlstream.matcher import StanzaPath
|
||||
@@ -71,8 +72,7 @@ class XEP_0199(BasePlugin):
|
||||
|
||||
if self.keepalive:
|
||||
self.xmpp.add_event_handler('session_start',
|
||||
self.enable_keepalive,
|
||||
threaded=True)
|
||||
self.enable_keepalive)
|
||||
self.xmpp.add_event_handler('session_end',
|
||||
self.disable_keepalive)
|
||||
|
||||
@@ -129,8 +129,7 @@ class XEP_0199(BasePlugin):
|
||||
timeout -- Time in seconds to wait for a response.
|
||||
Defaults to self.timeout.
|
||||
callback -- Optional handler to execute when a pong
|
||||
is received. Useful in conjunction with
|
||||
the option block=False.
|
||||
is received.
|
||||
"""
|
||||
if not timeout:
|
||||
timeout = self.timeout
|
||||
@@ -144,8 +143,10 @@ class XEP_0199(BasePlugin):
|
||||
return iq.send(timeout=timeout, callback=callback,
|
||||
timeout_callback=timeout_callback)
|
||||
|
||||
@asyncio.coroutine
|
||||
def ping(self, jid=None, ifrom=None, timeout=None):
|
||||
"""Send a ping request and calculate RTT.
|
||||
This is a coroutine.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID that will receive the ping.
|
||||
@@ -171,7 +172,8 @@ class XEP_0199(BasePlugin):
|
||||
|
||||
log.debug('Pinging %s' % jid)
|
||||
try:
|
||||
self.send_ping(jid, ifrom=ifrom, timeout=timeout)
|
||||
yield from self.send_ping(jid, ifrom=ifrom, timeout=timeout,
|
||||
coroutine=True)
|
||||
except IqError as e:
|
||||
if own_host:
|
||||
rtt = time.time() - start
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user