Compare commits

..

136 Commits

Author SHA1 Message Date
mathieui
c7bd224182 Update version to 1.5.2 2020-05-23 23:43:27 +02:00
mathieui
bac1e9b44a Merge branch 'muc-presence' into 'master'
xep_0045: don't create empty item

See merge request poezio/slixmpp!51
2020-05-22 16:32:27 +02:00
Maxime “pep” Buquet
b62f0e90c1 xep_0045: don't create empty item
Only create an item if an attribute is set. Don't create it when reading
if it wasn't already present.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-05-20 00:37:46 +02:00
mathieui
9ace053992 Merge branch 'upload-filetoobig' into 'master'
xep_0363: pass more information to FileTooBig exception

See merge request poezio/slixmpp!50
2020-05-12 01:53:00 +02:00
Maxime “pep” Buquet
c7cd2fcf33 xep_0363: pass more information to FileTooBig exception
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-05-12 01:49:56 +02:00
Maxime Buquet
e57289358f Merge branch 'upload-disco-form' into 'master'
xep_0363: Ensure every form in disco#info is read

See merge request poezio/slixmpp!49
2020-05-12 01:04:41 +02:00
Maxime “pep” Buquet
4aa35c11ab xep_0363: Ensure every form in disco#info is read
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-05-12 01:03:50 +02:00
mathieui
d6f7d32bbe Merge branch 'version-1.5.1' into 'master'
Update version to 1.5.1

See merge request poezio/slixmpp!47
2020-05-02 19:44:54 +02:00
mathieui
0dd32be7f5 Update version to 1.5.1
1.5.0 changed what could be expected to be sent before calling
  disconnect(), this fixes it.
2020-05-02 19:39:21 +02:00
Maxime Buquet
bf69698af1 Merge branch 'add-forever-false-examples' into 'master'
Add forever=False to some examples to make them terminate

See merge request poezio/slixmpp!46
2020-05-02 17:54:50 +02:00
Maxime Buquet
aa732b3c94 Merge branch 'fix-disconnect-send-queue' into 'master'
Fix a regression introduced in 1.5.0

See merge request poezio/slixmpp!45
2020-05-02 17:35:17 +02:00
mathieui
d076cef023 Add forever=False to some examples to make them terminate 2020-05-02 17:34:11 +02:00
mathieui
f884b67b8b Fix a regression introduced in 1.5.0
Due to the send queue, messages sent immediatly before calling
"disconnect" would not be sent.
2020-05-02 17:17:08 +02:00
Maxime Buquet
0d3116dbdf Merge branch 'fix-start-handlers' into 'master'
Change session_start callback to async in most examples

See merge request poezio/slixmpp!44
2020-05-02 16:51:51 +02:00
mathieui
f1ab9ab964 Change session_start callback to async in most examples
If we fetch the roster, we should probably wait until we get it back
2020-05-02 16:47:04 +02:00
mathieui
e520ab1f5e Merge branch 'release-1.5.0' into 'master'
Update version to 1.5.0

See merge request poezio/slixmpp!43
2020-05-01 16:00:58 +02:00
mathieui
3dcb96d9d8 Update version to 1.5.0 2020-05-01 15:26:24 +02:00
Maxime Buquet
0a7a4c3abe Merge branch 'python-version' into 'master'
Update Python version minimum requirement to 3.7

See merge request poezio/slixmpp!42
2020-05-01 15:14:49 +02:00
Maxime “pep” Buquet
a4bbc404ed Update Python version minimum requirement to 3.7
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-05-01 15:13:19 +02:00
Maxime “pep” Buquet
c3fbc6cb80 xep_0196: Use correct tag local name (thanks ivucica)
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-04-06 12:57:12 +02:00
Maxime “pep” Buquet
355d789061 docs: remove andyet logo/link on every page
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-04-06 12:57:12 +02:00
mathieui
47ed67c04e Merge branch 'add-github-pr-template' into 'master'
Add a github pull request template

See merge request poezio/slixmpp!41
2020-04-04 20:06:23 +02:00
mathieui
34567f450a Add a github pull request template 2020-04-04 20:04:23 +02:00
mathieui
9126bd8392 Merge branch 'fix-deprecations' into 'master'
Fix deprecation warning regarding invalid escape sequences.

See merge request poezio/slixmpp!40
2020-04-04 18:53:15 +02:00
mathieui
02202f7cd8 Merge branch 'fix-nonetype-error' into 'master'
Fix an issue when deleting subelements: TypeError: 'NoneType' object is not an iterator

See merge request poezio/slixmpp!39
2020-04-04 18:44:25 +02:00
jheling
2add94f5b0 Fix TypeError: 'NoneType' object is not an iterator
When deleting sub-elements in a stanza.
2020-04-04 18:42:48 +02:00
Karthikeyan Singaravelan
5fc757f200 Fix deprecation warning regarding invalid escape sequences. 2020-04-04 16:08:44 +00:00
mathieui
98108d0445 Merge branch 'fix-celementtree-import' into 'master'
cElementTree has been deprecated since Python 3.3 and removed in Python 3.9.

See merge request poezio/slixmpp!38
2020-04-04 17:55:11 +02:00
Maxime Buquet
76f4fb49d6 Merge branch 'sync-fixes' into 'master'
Sync fixes

See merge request poezio/slixmpp!37
2020-04-04 15:28:59 +02:00
Georg Lukas
5be46a5e68 fire 'disconnected' callback from abort() 2020-04-04 13:16:33 +02:00
Georg Lukas
ab9040c30e expose is_connecting state based on connection attempt future 2020-04-04 13:16:31 +02:00
Maxime Buquet
a16e2a0f6c Merge branch 'fix-0198' into 'master'
XEP-0198: unset end_session_on_disconnect on resume/enable

See merge request poezio/slixmpp!36
2020-03-29 14:28:06 +02:00
Maxime Buquet
842aa3be8f Merge branch 'fix-reconnect-2.0' into 'master'
Reset reconnect delay on manual reconnect, add delay event

See merge request poezio/slixmpp!35
2020-03-29 14:26:15 +02:00
Georg Lukas
6c28b49e7f XEP-0198: unset end_session_on_disconnect on resume/enable 2020-03-29 14:21:40 +02:00
Georg Lukas
621255027d Reset reconnect delay on manual reconnect, add delay event
This is just a hotfix workaround for an underlying problem. The
`_connect_routine` code is "blocking" (in an async way) for
`connect_loop_wait` seconds, so that a fresh-started manual reconnect
will be silenty delayed. This code does the following changes:

1. It moves the delay to before the DNS resolution (with the exponential
   back-off it might well be that the DNS records are changed while
   slixmpp is waiting).

2. It adds a new event `reconnect_delay` that gets passed the number of
   seconds it will delay before actually reconnecting

3. It resets the `connect_loop_wait` timer on a manual connect/reconnect
   call to fix the interactive experience.

A *proper fix* would replace the sleep in `_connect_routine` with a
properly timered re-invocation of it, but I don't understand enough of
asyncio for pulling off that magic, and this is actually a proper
improvement. Also I tested this and it works!
2020-03-29 14:21:05 +02:00
Maxime Buquet
efe316dc8c Merge branch 'fix-0198' into 'master'
XEP-0198: properly disable on disconnect, fix reconnect-loop

See merge request poezio/slixmpp!34
2020-03-28 22:11:52 +01:00
Maxime Buquet
e9a87a0b77 Merge branch 'master' into 'master'
reconnect: fix callback when not currently connected

See merge request poezio/slixmpp!32
2020-03-28 22:11:34 +01:00
Georg Lukas
85c9967b9c XEP-0198: fix race conditions on enable/resume
This code splits out the `enabled` property into `enabled_in` and
`enabled_out` to reflect that client and server enable 0198
asynchronously.

This also moves the actual enabling code into the stanza processing
logic, because apparently, `enable.send()` was just added into the end
of the send queue, but `enable` got enabled immediately, so that poezio
requested ACKs for whatever happened to be in the queue before.

Async is hard, let's go get fishing.
2020-03-28 21:49:18 +01:00
Georg Lukas
deb6d4f176 XEP-0198: properly disable on disconnect, fix reconnect-loop
When the connection is disconnected (but the session didn't "end",
because 0198 resumption is enabled), poezio will reconnect and try to
send an <r/> element because the 0198 plugin doesn't realize that SM
wasn't re-enabled yet.
2020-03-28 20:50:55 +01:00
Karthikeyan Singaravelan
7218bb4499 cElementTree has been deprecated since Python 3.3 and removed in Python 3.9. 2020-03-28 04:42:23 +00:00
Georg Lukas
d85efec7a2 reconnect: fix callback when not currently connected
The 'disconnected' event is normally fired from connection_lost(), which
is called by the connection code when the connection is lost after being
established. However, if the connection wasn't successfully established,
a manual /reconnect no-ops because it waits for the 'disconnected'
callback which never fires. This patch does two things:

1. Immediately fire a 'disconnected' event in disconnect() if there is
   no transport.

2. Register the 'disconnected' event handler in reconnect() *before* it
   can be fired.
2020-03-23 18:59:29 +01:00
mathieui
115c234527 Merge branch 'async-filters' into 'master'
Add async filters on the send process

See merge request poezio/slixmpp!24
2019-12-27 15:35:00 +01:00
mathieui
a0f5cb6e09 Put the queue exception at toplevel 2019-12-27 15:27:29 +01:00
mathieui
110bbf8afc Improve the send queue code a bit 2019-12-27 15:27:29 +01:00
mathieui
d97efa0bd8 add a separate place for slow ass filters 2019-12-27 15:27:29 +01:00
mathieui
672f1b28f6 raise Exception 2019-12-27 15:27:29 +01:00
mathieui
27d3ae958b Try/except around outbound stanza processing
to avoid killing the send loop when a filter has an error
2019-12-27 15:27:29 +01:00
mathieui
a32794ec35 Remove trailing whitespace 2019-12-27 15:27:28 +01:00
mathieui
aa11ba463e Skip 0323 because 2019-12-27 15:27:28 +01:00
mathieui
a83c00e933 Update test framework to work with new filters
(eewww)
2019-12-27 15:27:28 +01:00
mathieui
31f6ef6814 Run the send queue in a separate coroutine
To be able to run async stream filters
2019-12-27 15:27:25 +01:00
Maxime “pep” Buquet
9b3874b5df xep_0048: Ensure Conference jid is of type str when given a JID
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-11-11 14:00:19 +01:00
mathieui
0139fb291e Fix a bug in the 0202 plugin where a time result would send back a result
poezio bug #3499
2019-09-19 22:54:53 +02:00
mathieui
e58988484a Match the sender JID as well as the queryid in MAM results 2019-09-10 23:13:04 +02:00
mathieui
5d5e5cda19 Add typing/docstring in the MAM plugin 2019-09-10 22:44:46 +02:00
root
11f707987d Added amount parameter, so that limit on the msgs received per query can be changed. 2019-09-08 14:22:48 +02:00
Maxime “pep” Buquet
db13794e0f Revert "Remove a block of compatibility code"
This reverts commit 37bc1bb9b3.

Confusion confusion. Mathieui thought this was a sleekxmpp thing when
it's actually been added not so long ago.
2019-08-28 00:12:10 +02:00
mathieui
37bc1bb9b3 Remove a block of compatibility code
even if the user makes that mistake, it does not cause problems down the
line.
2019-08-26 12:00:38 +02:00
mathieui
9be30e5291 fix a typo in the invalidjid exception name case 2019-08-25 01:42:00 +02:00
Maxime Buquet
9fe20a4056 Merge branch 'origin-id' into 'master'
Implement Origin-id (XEP-0359)

See merge request poezio/slixmpp!21
2019-08-23 17:10:42 +02:00
Maxime “pep” Buquet
3253d34c0a basexmpp: Make origin-id opt-out
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-08-23 17:09:21 +02:00
Maxime “pep” Buquet
fef575ee1a Implement origin-id (XEP-0359)
This XEP is not implemented as a plugin but directly into Message.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-08-23 17:09:21 +02:00
Maxime Buquet
540ff89427 Merge branch 'mam' into 'master'
Assign 'True' to 'before' tag if it's value is 'None'.

See merge request poezio/slixmpp!26
2019-08-23 16:37:12 +02:00
root
dd8ac8fc87 Assign True to the 'before' tag if it's value is None (eg:at the start no msg is there in the group, so no stanza-id) 2019-08-23 04:39:20 +05:30
Maxime Buquet
2249d878d1 Merge branch 'mam' into 'master'
Removed assigning 'reverse' value to the 'before' tag

See merge request poezio/slixmpp!25
2019-08-23 00:03:07 +02:00
root
89fa9dc1dd Removed before tag. (Code for setting it is already there) 2019-08-23 00:21:42 +05:30
root
d7729e8683 Removed assigning 'reverse' value to the 'before' tag (It's value is set in xep_0313 (mam.py file) and if not then by default it is takes as 'None'). 2019-08-23 00:15:56 +05:30
louiz’
d618f55dea Merge branch 'mam' into 'master'
Added <before> tag for querying messages before a stanza-id.

See merge request poezio/slixmpp!23
2019-08-13 09:54:43 +02:00
Madhur Garg
b0e688eb35 Added <before> tag for querying messages before a stanza-id. 2019-08-12 23:39:01 +05:30
Maxime “pep” Buquet
0e7176483b xep_0030: add docstring to get_info_from_domain
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-08-03 16:05:09 +02:00
Emmanuel Gil Peyrot
f35569a2c1 Remove the last instances of a block argument to iq.send().
Thanks Madhur Garg for spotting this in
027ce2434d7fd3cf4a286dd373cb761c0d114c66!
2019-08-01 22:36:59 +02:00
Link Mauve
bec6f7c8f3 Merge branch 'mam' into 'master'
Removed 'block' from set_preferences as it was giving a TypeError while sending the staza.

See merge request poezio/slixmpp!20
2019-08-01 22:30:58 +02:00
Madhur Garg
027ce2434d Removed 'block' from set_preferences as it was giving a TypeError while sending the staza. 2019-08-02 01:57:36 +05:30
Maxime Buquet
d57fbb57a2 Merge branch 'mam' into 'master'
Added a function in xep313 plugin to get current MAM preferences.

See merge request poezio/slixmpp!19
2019-07-22 22:05:05 +02:00
Madhur Garg
85cd7a9166 Added a function to get current MAM preferences. 2019-07-18 16:55:11 +05:30
Maxime “pep” Buquet
d50d996c68 xmlstream/stanzabase: remove unused interfaces and types attributes
These are already on each stanza, and were not applicable to all stanzas
anyway.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-07-16 11:26:54 +02:00
mathieui
371ad20ca7 Do not add disco#info for occupantid, it’s a server thing 2019-07-14 13:25:22 +02:00
mathieui
5f49df6b56 Add the occupant id stanza elements 2019-07-14 13:13:59 +02:00
mathieui
b50bfb2f34 Initial commit for reactions protoxep 2019-07-13 19:44:32 +02:00
Maxime “pep” Buquet
b29bb30eb7 Make generated stanza id truly random
Fix long-standing security issues where stanza @id be predictable.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-07-13 14:07:31 +02:00
Maxime “pep” Buquet
4435c81d77 xmlstream.disconnect: add compat behaviour, set wait to default 2.0 when True is passed. Update documentation
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-07-03 21:36:17 +02:00
Madhur Garg
2638ba2744 Added 'reverse' argument. 2019-07-03 14:18:33 +05:30
Madhur Garg
dbc9758311 Added 'reverse' parameter in mam and rsm plugins 2019-07-03 10:31:18 +05:30
Maxime “pep” Buquet
47968963b1 Revert part of previous commit. Return NotImplemented when object is not a valid JID
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-05-07 11:56:39 +01:00
Maxime “pep” Buquet
4e8800f954 jid: return not equal if value can't be converted to JID
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-05-07 10:08:46 +01:00
Maxime “pep” Buquet
40053518aa Merge remote-tracking branch 'origin/mr/13' 2019-04-27 12:59:23 +01:00
Maxime “pep” Buquet
1ee0f72ead xmlstream.disconnect: fix frenchism in docstring
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-24 23:35:33 +01:00
Maxime “pep” Buquet
4bb81228ae xmlstream.disconnect: typing hints
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-24 23:35:06 +01:00
Maxime “pep” Buquet
60a7a5b8df presence: Ensure <show/> value is valid when returned as presence @type value
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-24 20:29:54 +01:00
Maxime “pep” Buquet
946674f424 xep_0045: Ensure <show/> value is valid.
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-24 20:28:58 +01:00
Maxime “pep” Buquet
412a9169bd Fixes #3432. Allow execute to be used with the meaning of 'next'.
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-14 02:34:22 +01:00
Maxime “pep” Buquet
72b355de8c xep_0050: Fix indentation
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-14 02:19:58 +01:00
Maxime “pep” Buquet
af246dcfe1 slixmpp/jid: add types
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-07 23:14:44 +01:00
Maxime Buquet
9612e518fb Merge branch 'master' into 'master'
Communicate the reason for a disconnect to the application

See merge request poezio/slixmpp!12
2019-04-07 00:24:50 +02:00
Maxime “pep” Buquet
fde8264191 xep_0202: Fix plugin_init docstring
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-06 13:44:05 +01:00
Maxime “pep” Buquet
1cdc656208 Fixes poezio/poezio#3472: Don't remove TZ in 0202 utc tag
<utc/> MUST conform to 0082 dateTime profile and thus include a
timezone definition.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-06 13:42:26 +01:00
Maxime “pep” Buquet
0042108a67 poezio/poezio#3472: Ensure tz is correctly set when offset is an int
Thanks lovetox!

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-03 23:16:37 +01:00
Georg Lukas
704161a285 mark end-of-stream as session-ending event 2019-03-26 15:16:52 +01:00
Georg Lukas
6b1b58a339 XEP-0199: use new 0-timeout reconnect() with reason 2019-03-26 12:09:43 +01:00
Georg Lukas
4f96e5fa75 Do not directly enqueue connect() as event handler, parameter mismatch 2019-03-26 12:09:43 +01:00
Georg Lukas
bcb90a653e Do not close stream on 0-timeout disconnect, allows 0198 resume 2019-03-26 11:02:28 +01:00
Georg Lukas
7e435b703d Propagate disconnect() reason into 'disconnected' event 2019-03-26 11:01:36 +01:00
Maxime “pep” Buquet
2dda6b80d4 Partially fix poezio/poezio#3452. Prevent groupchat_subject from triggered sent when body or thread are in the message.
0045 says:
> The subject is changed by sending a message of type "groupchat" to the
> <room@service>, where the <message/> MUST contain a <subject/> element that
> specifies the new subject but MUST NOT contain a <body/> element (or a
> <thread/> element). In accordance with the core definition of XMPP, other child
> elements are allowed (although the entity that receives them might ignore
> them).
>
> Note: A message with a <subject/> and a <body/> or a <subject/> and a <thread/>
> is a legitimate message, but it SHALL NOT be interpreted as a subject change.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-03-23 17:06:07 +00:00
mathieui
5629e44710 Merge branch 'patch-1' into 'master'
Fix slixmpp.ClientXMPP.cancel_connection_attempt()

See merge request poezio/slixmpp!10
2019-03-15 00:38:35 +01:00
Maxime “pep” Buquet
6a06881d8b xep_0030: fix typo on 'remote' in get_info docstring
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-03-05 00:00:14 +00:00
Maxime “pep” Buquet
2b666eb1de Revert "Remove license from unused project"
This reverts commit fbab3ad214.
2019-02-24 12:16:06 +00:00
Emmanuel Gil Peyrot
400e7a3903 Remove SocksiPy license
I wrote a SOCKS5 implementation back in
9c5dd024b1, and removed SocksiPy from
slixmpp’s codebase at the same time.
2019-02-24 13:02:08 +01:00
Maxime “pep” Buquet
fbab3ad214 Remove license from unused project
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-02-24 12:00:16 +00:00
mathieui
628b357b06 Merge branch 'eme-add-method' into 'master'
xep_0380: Add add_eme method

See merge request poezio/slixmpp!11
2019-02-23 14:07:15 +01:00
Maxime “pep” Buquet
88260cc240 xep_0380: Add add_eme method
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-02-23 12:54:56 +00:00
Maxime “pep” Buquet
e9f2f503b8 xep_0380: Remove remove_handler call in plugin_end
Remove probable copy paste fail in EME plugin_end handler.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-02-23 12:36:40 +00:00
ehendrix23
696a72247b Fix slixmpp.ClientXMPP.cancel_connection_attempt() 2019-02-22 00:41:02 +01:00
Maxime “pep” Buquet
05d76e4b1d Change more URLs to lab.louiz.org
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-02-12 10:34:57 +00:00
Maxime “pep” Buquet
d52d4fbbbe Update project URLs to lab.louiz.org
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-02-12 10:26:04 +00:00
Maxime “pep” Buquet
e53c0fcb30 README: Add pep as a contributor
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-02-10 14:18:28 +00:00
Maxime “pep” Buquet
97d68c5196 setup.py: GTalk is no more
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-02-10 14:18:28 +00:00
mathieui
b42fafabb4 Make the cache encode and decode not crash if something goes wrong 2019-02-02 17:42:24 +01:00
mathieui
3a44ec8f15 Add tests for the cache api 2019-02-02 17:32:10 +01:00
mathieui
93f385562f Add a "remove" action on the cache API 2019-02-02 17:31:48 +01:00
mathieui
9cab02438b Fix XEP-0335 2019-02-02 15:41:55 +01:00
Emmanuel Gil Peyrot
74ed50e626 Set @id by default on outgoing messages and presences.
Respects RFC6120 §8.1.3’s RECOMMENDED.
2019-01-31 16:46:51 +01:00
mathieui
9d378c611c Release 1.4.2 2019-01-31 14:50:26 +01:00
Link Mauve
d85d8f4479 Merge branch 'xep-0335' into 'master'
Add xep_0335: JSON Containers

See merge request poezio/slixmpp!5
2019-01-22 19:28:38 +01:00
Emmanuel Gil Peyrot
fb75f7cda9 Stop requesting avatar without the intervention of the client. 2019-01-22 15:12:00 +01:00
Emmanuel Gil Peyrot
41419a2161 Fix authenticating on a non-TLS socket.
This was broken since c1562b76b2.
2019-01-21 01:02:06 +01:00
Emmanuel Gil Peyrot
7cd73b594e XEP-0223: Fix default access_model, it MUST be whitelist. 2019-01-17 12:08:51 +01:00
Emmanuel Gil Peyrot
15c6b775ff Simplify the non-CDATA path of tostring.escape. 2019-01-09 15:03:05 +01:00
Emmanuel Gil Peyrot
4b482477e2 Split ns only once in fix_ns(). 2019-01-09 14:57:39 +01:00
Emmanuel Gil Peyrot
f7e4caadfe Split tag and attrib only once in tostring(). 2019-01-09 14:55:27 +01:00
Emmanuel Gil Peyrot
5f25b0b6a0 Add a default timeout to iq.send().
This fixes a leak of MatchIDSender in handlers, making it more and more
expensive to match stanzas as more iqs have been sent.
2019-01-09 14:20:31 +01:00
Mateusz Piotrowski
d228bc42ea Mention that GnuPG is required for tests 2018-12-27 17:21:12 +01:00
mathieui
ecdc44a601 Merge branch 'master' into 'master'
Decode bytes in GSSAPI handling, as expected by the kerberos module API.

See merge request poezio/slixmpp!8
2018-12-27 16:55:47 +01:00
Emmanuel Gil Peyrot
33370e42f1 XEP-0363: Use a specific exception for HTTP errors 2018-11-20 07:44:09 +01:00
Florian Klien
4699861925 catch http upload errors on upload 2018-11-20 07:34:56 +01:00
Jelmer Vernooij
2d228bdb56 Decode bytes in GSSAPI handling, as expected by the kerberos module API. 2018-10-30 22:29:20 +00:00
Maxime “pep” Buquet
31f5e84671 Add xep_0335: JSON Containers
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2018-09-16 22:13:41 +01:00
84 changed files with 901 additions and 332 deletions

13
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,13 @@
################ Please use Gitlab instead of Github ###################################
Hello, thank you for contributing to slixmpp!
Youre about to open a pull request on github. However this github repository is not the official place for contributions on slixmpp.
Please open your merge request on https://lab.louiz.org/poezio/slixmpp/
You should be able to log in there with your github credentials, clone the slixmpp repository in your namespace, push your existing pull request into a new branch, and then open a merge request with one click, within 3 minutes.
This will help us review your contribution, avoid spreading things everywhere and it will even run the tests automatically with your changes.
Thank you.

View File

@@ -1,9 +1,7 @@
language: python
python:
- "3.4"
- "3.5"
- "3.6"
- "3.7-dev"
- "3.7"
- "3.8-dev"
install:
- "pip install ."
script: testall.py

View File

@@ -5,7 +5,7 @@ To contribute, the preferred way is to commit your changes on some
publicly-available git repository (on a fork `on github
<https://github.com/poezio/slixmpp>`_ or on your own repository) and to
notify the developers with either:
- a ticket `on the bug tracker <https://dev.poez.io/new>`_
- a ticket `on the bug tracker <https://lab.louiz.org/poezio/slixmpp/issues/new>`_
- a pull request on github
- a simple message on `the XMPP MUC <xmpp:slixmpp@muc.poez.io>`_

View File

@@ -1,6 +1,7 @@
Pre-requisites:
- Python 3.5+
- Python 3.7+
- Cython 0.22 and libidn, optionally (making JID faster by compiling the stringprep module)
- GnuPG, for testing
Install:
> python3 setup.py install

25
LICENSE
View File

@@ -167,28 +167,3 @@ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
socksipy: A Python SOCKS client module.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Copyright 2006 Dan-Haim. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of Dan Haim nor the names of his contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.

View File

@@ -1,7 +1,7 @@
Slixmpp
#########
Slixmpp is an MIT licensed XMPP library for Python 3.5+. It is a fork of
Slixmpp is an MIT licensed XMPP library for Python 3.7+. It is a fork of
SleekXMPP.
Slixmpp's goals is to only rewrite the core of the library (the low level
@@ -113,6 +113,7 @@ Slixmpp Credits
- Gasper Zejn (`Gasper Zejn <mailto:zejn@kiberpipa.org>`_)
- Krzysztof Kotlenga (`Krzysztof Kotlenga <mailto:pocek@users.sf.net>`_)
- Tsukasa Hiiragi (`Tsukasa Hiiragi <mailto:bakalolka@gmail.com>`_)
- Maxime Buquet (`pep <xmpp:pep@bouah.net?message>`_)
Credits (SleekXMPP)
-------------------

View File

@@ -408,24 +408,3 @@ div.viewcode-block:target {
margin: -1px -12px;
padding: 0 12px;
}
#from_andyet {
-webkit-box-shadow: #CCC 0px 0px 3px;
background: rgba(255, 255, 255, 1);
bottom: 0px;
right: 17px;
padding: 3px 10px;
position: fixed;
}
#from_andyet h2 {
background-image: url("images/from_&yet.png");
background-repeat: no-repeat;
height: 29px;
line-height: 0;
text-indent: -9999em;
width: 79px;
margin-top: 0;
margin: 0px;
padding: 0px;
}

View File

@@ -65,6 +65,5 @@
<div class="bottomnav">
{{ nav() }}
</div>
<a id="from_andyet" href="http://andyet.net"><h2>From &amp;yet</h2></a>
{% endblock %}

View File

@@ -13,7 +13,7 @@ hides namespaces when able and does not introduce excessive namespace
prefixes::
>>> from slixmpp.xmlstream.tostring import tostring
>>> from xml.etree import cElementTree as ET
>>> from xml.etree import ElementTree as ET
>>> xml = ET.fromstring('<foo xmlns="bar"><baz /></foo>')
>>> ET.tostring(xml)
'<ns0:foo xmlns:ns0="bar"><ns0:baz /></foo>'

View File

@@ -3,8 +3,9 @@
Differences from SleekXMPP
==========================
**Python 3.5+ only**
slixmpp will only work on python 3.5 and above.
**Python 3.7+ only**
slixmpp will work on python 3.7 and above. It may work with previous
versions but we provide no guarantees.
**Stanza copies**
The same stanza object is given through all the handlers; a handler that

View File

@@ -11,7 +11,7 @@ Create and Run a Server Component
<xmpp:slixmpp@muc.poez.io?join>`_.
If you have not yet installed Slixmpp, do so now by either checking out a version
with `Git <http://git.poez.io/slixmpp>`_.
with `Git <https://lab.louiz.org/poezio/slixmpp>`_.
Many XMPP applications eventually graduate to requiring to run as a server
component in order to meet scalability requirements. To demonstrate how to

View File

@@ -11,7 +11,7 @@ Slixmpp Quickstart - Echo Bot
<xmpp:slixmpp@muc.poez.io?join>`_.
If you have not yet installed Slixmpp, do so now by either checking out a version
with `Git <http://git.poez.io/slixmpp>`_.
with `Git <https://lab.louiz.org/poezio/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
@@ -329,7 +329,7 @@ The Final Product
-----------------
Here then is what the final result should look like after working through the guide above. The code
can also be found in the Slixmpp `examples directory <http://git.poez.io/slixmpp/tree/examples>`_.
can also be found in the Slixmpp `examples directory <https://lab.louiz.org/poezio/slixmpp/tree/master/examples>`_.
.. compound::

View File

@@ -11,7 +11,7 @@ Multi-User Chat (MUC) Bot
<xmpp:slixmpp@muc.poez.io?join>`_.
If you have not yet installed Slixmpp, do so now by either checking out a version
from `Git <http://git.poez.io/slixmpp>`_.
from `Git <https://lab.louiz.org/poezio/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

View File

@@ -47,11 +47,11 @@ the roster. Next, we want to send our message, and to do that we will use :meth:
self.send_message(mto=self.recipient, mbody=self.msg)
Finally, we need to disconnect the client using :meth:`disconnect <slixmpp.xmlstream.XMLStream.disconnect>`.
Now, sent stanzas are placed in a queue to pass them to the send thread. If we were to call
:meth:`disconnect <slixmpp.xmlstream.XMLStream.disconnect>` without any parameters, then it is possible
for the client to disconnect before the send queue is processed and the message is actually
sent on the wire. To ensure that our message is processed, we use
:meth:`disconnect(wait=True) <slixmpp.xmlstream.XMLStream.disconnect>`.
Now, sent stanzas are placed in a queue to pass them to the send thread.
:meth:`disconnect <slixmpp.xmlstream.XMLStream.disconnect>` by default will wait for an
acknowledgement from the server for at least `2.0` seconds. This time is configurable with
the `wait` parameter. If `0.0` is passed for `wait`, :meth:`disconnect
<slixmpp.xmlstream.XMLStream.disconnect>` will not close the connection gracefully.
.. code-block:: python
@@ -61,12 +61,12 @@ sent on the wire. To ensure that our message is processed, we use
self.send_message(mto=self.recipient, mbody=self.msg)
self.disconnect(wait=True)
self.disconnect()
.. warning::
If you happen to be adding stanzas to the send queue faster than the send thread
can process them, then :meth:`disconnect(wait=True) <slixmpp.xmlstream.XMLStream.disconnect>`
can process them, then :meth:`disconnect() <slixmpp.xmlstream.XMLStream.disconnect>`
will block and not disconnect.
Final Product

View File

@@ -4,9 +4,9 @@ Slixmpp
.. sidebar:: Get the Code
The latest source code for Slixmpp may be found on the `Git repo
<http://git.poez.io/slixmpp>`_. ::
<https://lab.louiz.org/poezio/slixmpp>`_. ::
git clone git://git.poez.io/slixmpp
git clone https://lab.louiz.org/poezio/slixmpp
An XMPP chat room is available for discussing and getting help with slixmpp.
@@ -14,14 +14,14 @@ Slixmpp
`slixmpp@muc.poez.io <xmpp:slixmpp@muc.poez.io?join>`_
**Reporting bugs**
You can report bugs at http://dev.louiz.org/projects/slixmpp/issues.
You can report bugs at http://lab.louiz.org/poezio/slixmpp/issues.
.. 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.5+,
Slixmpp is an :ref:`MIT licensed <license>` XMPP library for Python 3.7+,
Slixmpp's design goals and philosphy are:

View File

@@ -33,7 +33,7 @@ class CommandBot(slixmpp.ClientXMPP):
# our roster.
self.add_event_handler("session_start", self.start)
def start(self, event):
async def start(self, event):
"""
Process the session_start event.
@@ -47,7 +47,7 @@ class CommandBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
# We add the command after session_start has fired
# to ensure that the correct full JID is used.

View File

@@ -37,7 +37,7 @@ class CommandUserBot(slixmpp.ClientXMPP):
self.add_event_handler("session_start", self.start)
self.add_event_handler("message", self.message)
def start(self, event):
async def start(self, event):
"""
Process the session_start event.
@@ -51,7 +51,7 @@ class CommandUserBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
# We first create a session dictionary containing:
# 'next' -- the handler to execute on a successful response
@@ -176,4 +176,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
xmpp.process()
xmpp.process(forever=False)

View File

@@ -30,7 +30,7 @@ class AdminCommands(slixmpp.ClientXMPP):
self.add_event_handler("session_start", self.start)
def start(self, event):
async def start(self, event):
"""
Process the session_start event.
@@ -44,7 +44,7 @@ class AdminCommands(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
def command_success(iq, session):
print('Command completed')

View File

@@ -69,7 +69,7 @@ class Disco(slixmpp.ClientXMPP):
event does not provide any additional
data.
"""
self.get_roster()
await self.get_roster()
self.send_presence()
try:

View File

@@ -159,4 +159,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
xmpp.process()
xmpp.process(forever=False)

View File

@@ -38,7 +38,7 @@ class EchoBot(slixmpp.ClientXMPP):
# MUC messages and error messages.
self.add_event_handler("message", self.message)
def start(self, event):
async def start(self, event):
"""
Process the session_start event.
@@ -52,7 +52,7 @@ class EchoBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
def message(self, msg):
"""

View File

@@ -58,7 +58,7 @@ class GTalkBot(slixmpp.ClientXMPP):
logging.error(err.message)
self.disconnect()
def start(self, event):
async def start(self, event):
"""
Process the session_start event.
@@ -72,7 +72,7 @@ class GTalkBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
def message(self, msg):
"""

View File

@@ -39,7 +39,7 @@ class EchoBot(slixmpp.ClientXMPP):
# MUC messages and error messages.
self.add_event_handler("message", self.message)
def start(self, event):
async def start(self, event):
"""
Process the session_start event.
@@ -53,7 +53,7 @@ class EchoBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
def message(self, msg):
"""

View File

@@ -104,4 +104,4 @@ def on_session2(event):
new_xmpp.add_event_handler('session_start', on_session2)
new_xmpp.connect()
new_xmpp.process()
new_xmpp.process(forever=False)

View File

@@ -52,7 +52,7 @@ class MUCBot(slixmpp.ClientXMPP):
self.muc_online)
def start(self, event):
async def start(self, event):
"""
Process the session_start event.
@@ -65,7 +65,7 @@ class MUCBot(slixmpp.ClientXMPP):
event does not provide any additional
data.
"""
self.get_roster()
await self.get_roster()
self.send_presence()
self.plugin['xep_0045'].join_muc(self.room,
self.nick,

View File

@@ -51,7 +51,7 @@ class PingTest(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
try:
rtt = await self['xep_0199'].ping(self.pingjid,
@@ -109,4 +109,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
xmpp.process()
xmpp.process(forever=False)

View File

@@ -38,7 +38,7 @@ class EchoBot(slixmpp.ClientXMPP):
# MUC messages and error messages.
self.add_event_handler("message", self.message)
def start(self, event):
async def start(self, event):
"""
Process the session_start event.
@@ -52,7 +52,7 @@ class EchoBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
def message(self, msg):
"""

View File

@@ -32,7 +32,7 @@ class PubsubClient(slixmpp.ClientXMPP):
self.add_event_handler('session_start', self.start)
async def start(self, event):
self.get_roster()
await self.get_roster()
self.send_presence()
try:

View File

@@ -38,8 +38,8 @@ class PubsubEvents(slixmpp.ClientXMPP):
# self.add_event_handler('event_prefix_purge', handler)
# self.add_event_handler('event_prefix_delete', handler)
def start(self, event):
self.get_roster()
async def start(self, event):
await self.get_roster()
self.send_presence()
def _publish(self, msg):

View File

@@ -47,7 +47,7 @@ class RegisterBot(slixmpp.ClientXMPP):
# for data forms and OOB links that will make that easier.
self.add_event_handler("register", self.register)
def start(self, event):
async def start(self, event):
"""
Process the session_start event.
@@ -61,7 +61,7 @@ class RegisterBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
# We're only concerned about registering, so nothing more to do here.
self.disconnect()

View File

@@ -51,12 +51,8 @@ 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(callback=callback)
await future
await self.get_roster()
except IqError as err:
print('Error: %s' % err.iq['error']['condition'])
except IqTimeout:
@@ -138,4 +134,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
xmpp.process()
xmpp.process(forever=False)

View File

@@ -38,7 +38,7 @@ class SendMsgBot(slixmpp.ClientXMPP):
# our roster.
self.add_event_handler("session_start", self.start)
def start(self, event):
async def start(self, event):
"""
Process the session_start event.
@@ -52,7 +52,7 @@ class SendMsgBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
self.send_message(mto=self.recipient,
mbody=self.msg,
@@ -107,4 +107,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
xmpp.process()
xmpp.process(forever=False)

View File

@@ -46,7 +46,7 @@ class AvatarSetter(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
avatar_file = None
try:
@@ -137,4 +137,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
xmpp.process()
xmpp.process(forever=False)

View File

@@ -60,7 +60,7 @@ class ThirdPartyAuthBot(slixmpp.ClientXMPP):
# MUC messages and error messages.
self.add_event_handler("message", self.message)
def start(self, event):
async def start(self, event):
"""
Process the session_start event.
@@ -74,7 +74,7 @@ class ThirdPartyAuthBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
self.get_roster()
await self.get_roster()
def message(self, msg):
"""

View File

@@ -38,9 +38,9 @@ class LocationBot(ClientXMPP):
self.current_tune = None
def start(self, event):
async def start(self, event):
self.send_presence()
self.get_roster()
await self.get_roster()
self['xep_0115'].update_caps()
print("Using freegeoip.net to get geolocation.")

View File

@@ -35,9 +35,9 @@ class TuneBot(ClientXMPP):
self.current_tune = None
def start(self, event):
async def start(self, event):
self.send_presence()
self.get_roster()
await self.get_roster()
self['xep_0115'].update_caps()
def _update_tune(self):

View File

@@ -20,8 +20,7 @@ from run_tests import TestCommand
from slixmpp.version import __version__
VERSION = __version__
DESCRIPTION = ('Slixmpp is an elegant Python library for XMPP (aka Jabber, '
'Google Talk, etc).')
DESCRIPTION = ('Slixmpp is an elegant Python library for XMPP (aka Jabber).')
with open('README.rst', encoding='utf8') as readme:
LONG_DESCRIPTION = readme.read()
@@ -29,9 +28,8 @@ CLASSIFIERS = [
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Topic :: Internet :: XMPP',
'Topic :: Software Development :: Libraries :: Python Modules',
]
@@ -79,7 +77,7 @@ setup(
long_description=LONG_DESCRIPTION,
author='Florent Le Coz',
author_email='louiz@louiz.org',
url='https://dev.louiz.org/projects/slixmpp',
url='https://lab.louiz.org/poezio/slixmpp',
license='MIT',
platforms=['any'],
packages=packages,

View File

@@ -104,12 +104,15 @@ class BaseXMPP(XMLStream):
#: :attr:`use_message_ids` to `True` will assign all outgoing
#: messages an ID. Some plugin features require enabling
#: this option.
self.use_message_ids = False
self.use_message_ids = True
#: Presence updates may optionally be tagged with ID values.
#: Setting :attr:`use_message_ids` to `True` will assign all
#: outgoing messages an ID.
self.use_presence_ids = False
self.use_presence_ids = True
#: XEP-0359 <origin-id/> tag that gets added to <message/> stanzas.
self.use_origin_id = True
#: The API registry is a way to process callbacks based on
#: JID+node combinations. Each callback in the registry is

View File

@@ -97,7 +97,10 @@ class FeatureMechanisms(BasePlugin):
jid = self.xmpp.requested_jid.bare
result[value] = creds.get('email', jid)
elif value == 'channel_binding':
result[value] = self.xmpp.socket.get_channel_binding()
if isinstance(self.xmpp.socket, (ssl.SSLSocket, ssl.SSLObject)):
result[value] = self.xmpp.socket.get_channel_binding()
else:
result[value] = None
elif value == 'host':
result[value] = creds.get('host', self.xmpp.requested_jid.domain)
elif value == 'realm':

View File

@@ -16,6 +16,7 @@ import socket
from copy import deepcopy
from functools import lru_cache
from typing import Optional
from slixmpp.stringprep import nodeprep, resourceprep, idna, StringprepError
@@ -71,7 +72,7 @@ def _parse_jid(data):
return node, domain, resource
def _validate_node(node):
def _validate_node(node: Optional[str]):
"""Validate the local, or username, portion of a JID.
:raises InvalidJID:
@@ -93,7 +94,7 @@ def _validate_node(node):
return node
def _validate_domain(domain):
def _validate_domain(domain: str):
"""Validate the domain portion of a JID.
IP literal addresses are left as-is, if valid. Domain names
@@ -152,7 +153,7 @@ def _validate_domain(domain):
return domain
def _validate_resource(resource):
def _validate_resource(resource: Optional[str]):
"""Validate the resource portion of a JID.
:raises InvalidJID:
@@ -174,7 +175,7 @@ def _validate_resource(resource):
return resource
def _unescape_node(node):
def _unescape_node(node: str):
"""Unescape a local portion of a JID.
.. note::
@@ -199,7 +200,11 @@ def _unescape_node(node):
return ''.join(unescaped)
def _format_jid(local=None, domain=None, resource=None):
def _format_jid(
local: Optional[str] = None,
domain: Optional[str] = None,
resource: Optional[str] = None,
):
"""Format the given JID components into a full or bare JID.
:param string local: Optional. The local portion of the JID.
@@ -237,12 +242,17 @@ class UnescapedJID:
__slots__ = ('_node', '_domain', '_resource')
def __init__(self, node, domain, resource):
def __init__(
self,
node: Optional[str],
domain: Optional[str],
resource: Optional[str],
):
self._node = node
self._domain = domain
self._resource = resource
def __getattribute__(self, name):
def __getattribute__(self, name: str):
"""Retrieve the given JID component.
:param name: one of: user, server, domain, resource,
@@ -301,7 +311,7 @@ class JID:
__slots__ = ('_node', '_domain', '_resource', '_bare', '_full')
def __init__(self, jid=None):
def __init__(self, jid: Optional[str] = None):
if not jid:
self._node = ''
self._domain = ''
@@ -363,17 +373,17 @@ class JID:
return self._full
@node.setter
def node(self, value):
def node(self, value: str):
self._node = _validate_node(value)
self._update_bare_full()
@domain.setter
def domain(self, value):
def domain(self, value: str):
self._domain = _validate_domain(value)
self._update_bare_full()
@bare.setter
def bare(self, value):
def bare(self, value: str):
node, domain, resource = _parse_jid(value)
assert not resource
self._node = node
@@ -381,12 +391,12 @@ class JID:
self._update_bare_full()
@resource.setter
def resource(self, value):
def resource(self, value: str):
self._resource = _validate_resource(value)
self._update_bare_full()
@full.setter
def full(self, value):
def full(self, value: str):
self._node, self._domain, self._resource = _parse_jid(value)
self._update_bare_full()
@@ -413,7 +423,10 @@ class JID:
if isinstance(other, UnescapedJID):
return False
if not isinstance(other, JID):
other = JID(other)
try:
other = JID(other)
except InvalidJID:
return NotImplemented
return (self._node == other._node and
self._domain == other._domain and

View File

@@ -85,4 +85,6 @@ __all__ = [
'xep_0323', # IoT Systems Sensor Data
'xep_0325', # IoT Systems Control
'xep_0332', # HTTP Over XMPP Transport
'protoxep_reactions', # https://dino.im/xeps/reactions.html
'protoxep_occupantid', # https://dino.im/xeps/occupant-id.html
]

View File

@@ -0,0 +1,12 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.protoxep_occupantid.occupantid import XEP_OccupantID
from slixmpp.plugins.protoxep_occupantid.stanza import OccupantID
register_plugin(XEP_OccupantID)

View File

@@ -0,0 +1,23 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins import BasePlugin
from slixmpp.stanza import Message, Presence
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins.protoxep_occupantid import stanza
class XEP_OccupantID(BasePlugin):
name = 'protoxep_occupantid'
description = 'XEP-XXXX: Anonymous unique occupant identifiers for MUCs'
dependencies = set()
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Message, stanza.OccupantID)
register_stanza_plugin(Presence, stanza.OccupantID)

View File

@@ -0,0 +1,16 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase
class OccupantID(ElementBase):
name = 'occupant-id'
plugin_attrib = 'occupant-id'
namespace = 'urn:xmpp:occupant-id:0'
interfaces = {'id'}

View File

@@ -0,0 +1,11 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.protoxep_reactions.reactions import XEP_Reactions
register_plugin(XEP_Reactions)

View File

@@ -0,0 +1,54 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from typing import Iterable
from slixmpp.plugins import BasePlugin
from slixmpp.stanza import Message
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.matcher import MatchXMLMask
from slixmpp.xmlstream.handler import Callback
from slixmpp.plugins.protoxep_reactions import stanza
class XEP_Reactions(BasePlugin):
name = 'protoxep_reactions'
description = 'XEP-XXXX: Message Reactions'
dependencies = {'xep_0030'}
stanza = stanza
def plugin_init(self):
self.xmpp.register_handler(
Callback(
'Reaction received',
MatchXMLMask('<message><reactions xmlns="urn:xmpp:reactions:0"/></message>'),
self._handle_reactions,
)
)
self.xmpp['xep_0030'].add_feature('urn:xmpp:reactions:0')
register_stanza_plugin(Message, stanza.Reactions)
def plugin_end(self):
self.xmpp.remove_handler('Reaction received')
self.xmpp['xep_0030'].remove_feature('urn:xmpp:reactions:0')
def _handle_reactions(self, message: Message):
self.xmpp.event('reactions', message)
@staticmethod
def set_reactions(message: Message, to_id: str, reactions: Iterable[str]):
"""
Add reactions to a Message object.
"""
reactions_stanza = stanza.Reactions()
reactions_stanza['to'] = to_id
for reaction in reactions:
reaction_stanza = stanza.Reaction()
reaction_stanza['value'] = reaction
reactions_stanza.append(reaction_stanza)
message.append(reactions_stanza)

View File

@@ -0,0 +1,31 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
class Reactions(ElementBase):
name = 'reactions'
plugin_attrib = 'reactions'
namespace = 'urn:xmpp:reactions:0'
interfaces = {'to'}
class Reaction(ElementBase):
name = 'reaction'
namespace = 'urn:xmpp:reactions:0'
interfaces = {'value'}
def get_value(self) -> str:
return self.xml.text
def set_value(self, value: str):
self.xml.text = value
register_stanza_plugin(Reactions, Reaction, iterable=True)

View File

@@ -7,7 +7,7 @@
"""
from slixmpp.xmlstream.stanzabase import ElementBase
from xml.etree import cElementTree as ET
from xml.etree import ElementTree as ET
class RPCQuery(ElementBase):

View File

@@ -300,6 +300,8 @@ class XEP_0030(BasePlugin):
async def get_info_from_domain(self, domain=None, timeout=None,
cached=True, callback=None):
"""Fetch disco#info of specified domain and one disco#items level below"""
if domain is None:
domain = self.xmpp.boundjid.domain
@@ -348,7 +350,7 @@ class XEP_0030(BasePlugin):
combination handled by this Slixmpp instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
remote JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache

View File

@@ -9,7 +9,7 @@ from __future__ import with_statement
import logging
from slixmpp import Presence
from slixmpp import Presence, Message
from slixmpp.plugins import BasePlugin, register_plugin
from slixmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET
from slixmpp.xmlstream.handler.callback import Callback
@@ -29,61 +29,61 @@ class MUCPresence(ElementBase):
affiliations = {'', }
roles = {'', }
def get_xml_item(self):
def get_item_attr(self, attr, default):
item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
if item is None:
return default
return item.get(attr)
def set_item_attr(self, attr, value):
item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
if item is None:
item = ET.Element('{http://jabber.org/protocol/muc#user}item')
self.xml.append(item)
item.attrib[attr] = value
return item
def del_item_attr(self, attr):
item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
if item is not None and attr in item.attrib:
del item.attrib[attr]
def get_affiliation(self):
#TODO if no affilation, set it to the default and return default
item = self.get_xml_item()
return item.get('affiliation', '')
return self.get_item_attr('affiliation', '')
def set_affiliation(self, value):
item = self.get_xml_item()
#TODO check for valid affiliation
item.attrib['affiliation'] = value
self.set_item_attr('affiliation', value)
return self
def del_affiliation(self):
item = self.get_xml_item()
#TODO set default affiliation
if 'affiliation' in item.attrib: del item.attrib['affiliation']
# TODO: set default affiliation
self.del_item_attr('affiliation')
return self
def get_jid(self):
item = self.get_xml_item()
return JID(item.get('jid', ''))
return JID(self.get_item_attr('jid', ''))
def set_jid(self, value):
item = self.get_xml_item()
if not isinstance(value, str):
value = str(value)
item.attrib['jid'] = value
self.set_item_attr('jid', value)
return self
def del_jid(self):
item = self.get_xml_item()
if 'jid' in item.attrib: del item.attrib['jid']
self.del_item_attr('jid')
return self
def get_role(self):
item = self.get_xml_item()
#TODO get default role, set default role if none
return item.get('role', '')
return self.get_item_attr('role', '')
def set_role(self, value):
item = self.get_xml_item()
#TODO check for valid role
item.attrib['role'] = value
# TODO: check for valid role
self.set_item_attr('role', value)
return self
def del_role(self):
item = self.get_xml_item()
#TODO set default role
if 'role' in item.attrib: del item.attrib['role']
# TODO: set default role
self.del_item_attr('role')
return self
def get_nick(self):
@@ -162,7 +162,7 @@ class XEP_0045(BasePlugin):
return
self.xmpp.roster[pr['from']].ignore_updates = True
entry = pr['muc'].get_stanza_values()
entry['show'] = pr['show']
entry['show'] = pr['show'] if pr['show'] in pr.showtypes else None
entry['status'] = pr['status']
entry['alt_nick'] = pr['nick']
if pr['type'] == 'unavailable':
@@ -181,7 +181,7 @@ class XEP_0045(BasePlugin):
if got_online:
self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
def handle_groupchat_message(self, msg):
def handle_groupchat_message(self, msg: Message) -> None:
""" Handle a message event in a muc.
"""
self.xmpp.event('groupchat_message', msg)
@@ -195,10 +195,14 @@ class XEP_0045(BasePlugin):
def handle_groupchat_subject(self, msg):
def handle_groupchat_subject(self, msg: Message) -> None:
""" Handle a message coming from a muc indicating
a change of subject (or announcing it when joining the room)
"""
# See poezio#3452. A message containing subject _and_ (body or thread)
# is not a subject change.
if msg['body'] or msg['thread']:
return None
self.xmpp.event('groupchat_subject', msg)
def jid_in_room(self, room, jid):

View File

@@ -6,6 +6,7 @@
See the file LICENSE for copying permission.
"""
from slixmpp import JID
from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin
@@ -52,6 +53,12 @@ class Conference(ElementBase):
if value in ('1', 'true', True):
self._set_attr('autojoin', 'true')
def set_jid(self, value):
del self['jid']
if isinstance(value, JID):
value = value.full
self._set_attr('jid', value)
class URL(ElementBase):
name = 'url'

View File

@@ -89,31 +89,17 @@ class XEP_0050(BasePlugin):
self.commands = {}
self.xmpp.register_handler(
Callback("Ad-Hoc Execute",
StanzaPath('iq@type=set/command'),
self._handle_command))
Callback("Ad-Hoc Execute",
StanzaPath('iq@type=set/command'),
self._handle_command))
register_stanza_plugin(Iq, Command)
register_stanza_plugin(Command, Form, iterable=True)
self.xmpp.add_event_handler('command_execute',
self._handle_command_start)
self.xmpp.add_event_handler('command_next',
self._handle_command_next)
self.xmpp.add_event_handler('command_cancel',
self._handle_command_cancel)
self.xmpp.add_event_handler('command_complete',
self._handle_command_complete)
self.xmpp.add_event_handler('command', self._handle_command_all)
def plugin_end(self):
self.xmpp.del_event_handler('command_execute',
self._handle_command_start)
self.xmpp.del_event_handler('command_next',
self._handle_command_next)
self.xmpp.del_event_handler('command_cancel',
self._handle_command_cancel)
self.xmpp.del_event_handler('command_complete',
self._handle_command_complete)
self.xmpp.del_event_handler('command', self._handle_command_all)
self.xmpp.remove_handler('Ad-Hoc Execute')
self.xmpp['xep_0030'].del_feature(feature=Command.namespace)
self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple())
@@ -201,8 +187,27 @@ class XEP_0050(BasePlugin):
def _handle_command(self, iq):
"""Raise command events based on the command action."""
self.xmpp.event('command', iq)
self.xmpp.event('command_%s' % iq['command']['action'], iq)
def _handle_command_all(self, iq: Iq) -> None:
action = iq['command']['action']
sessionid = iq['command']['sessionid']
session = self.sessions.get(sessionid)
if session is None:
return self._handle_command_start(iq)
if action in ('next', 'execute'):
return self._handle_command_next(iq)
if action == 'prev':
return self._handle_command_prev(iq)
if action == 'complete':
return self._handle_command_complete(iq)
if action == 'cancel':
return self._handle_command_cancel(iq)
return None
def _handle_command_start(self, iq):
"""
Process an initial request to execute a command.
@@ -468,7 +473,7 @@ class XEP_0050(BasePlugin):
**kwargs)
def send_command(self, jid, node, ifrom=None, action='execute',
payload=None, sessionid=None, flow=False, **kwargs):
payload=None, sessionid=None, flow=False, **kwargs):
"""
Create and send a command stanza, without using the provided
workflow management APIs.

View File

@@ -79,7 +79,8 @@ class ResultIterator:
"""
if self._stop:
raise StopAsyncIteration
self.query[self.interface]['rsm']['before'] = self.reverse
if self.query[self.interface]['rsm']['before'] is None:
self.query[self.interface]['rsm']['before'] = self.reverse
self.query['id'] = self.query.stream.new_id()
self.query[self.interface]['rsm']['max'] = str(self.amount)
@@ -141,7 +142,7 @@ class XEP_0059(BasePlugin):
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Set.namespace)
def iterate(self, stanza, interface, results='substanzas',
def iterate(self, stanza, interface, results='substanzas', amount=10, reverse=False,
recv_interface=None, pre_cb=None, post_cb=None):
"""
Create a new result set iterator for a given stanza query.
@@ -169,6 +170,6 @@ class XEP_0059(BasePlugin):
results -- The name of the interface containing the
query results (typically just 'substanzas').
"""
return ResultIterator(stanza, interface, results,
return ResultIterator(stanza, interface, results, amount, reverse=reverse,
recv_interface=recv_interface, pre_cb=pre_cb,
post_cb=post_cb)

View File

@@ -130,7 +130,7 @@ def time(hour=None, min=None, sec=None, micro=None, offset=None, obj=False):
sec = now.second
if micro is None:
micro = now.microsecond
if offset is None:
if offset in (None, 0):
offset = tzutc()
elif not isinstance(offset, dt.tzinfo):
offset = tzoffset(None, offset)
@@ -177,7 +177,7 @@ def datetime(year=None, month=None, day=None, hour=None,
sec = now.second
if micro is None:
micro = now.microsecond
if offset is None:
if offset in (None, 0):
offset = tzutc()
elif not isinstance(offset, dt.tzinfo):
offset = tzoffset(None, offset)

View File

@@ -167,10 +167,7 @@ class XEP_0153(BasePlugin):
data = pres['vcard_temp_update']['photo']
if data is None:
return
elif data == '' or data != self.api['get_hash'](pres['from']):
ifrom = pres['to'] if self.xmpp.is_component else None
self.api['reset_hash'](pres['from'], ifrom=ifrom)
self.xmpp.event('vcard_avatar_update', pres)
self.xmpp.event('vcard_avatar_update', pres)
# =================================================================

View File

@@ -11,10 +11,9 @@ from slixmpp.xmlstream import ElementBase, ET
class UserGaming(ElementBase):
name = 'gaming'
name = 'game'
namespace = 'urn:xmpp:gaming:0'
plugin_attrib = 'gaming'
interfaces = {'character_name', 'character_profile', 'name',
'level', 'server_address', 'server_name', 'uri'}
sub_interfaces = interfaces

View File

@@ -71,7 +71,8 @@ class XEP_0198(BasePlugin):
self.window_counter = self.window
self.enabled = False
self.enabled_in = False
self.enabled_out = False
self.unacked_queue = collections.deque()
register_stanza_plugin(StreamFeatures, stanza.StreamManagement)
@@ -82,10 +83,6 @@ class XEP_0198(BasePlugin):
self.xmpp.register_stanza(stanza.Ack)
self.xmpp.register_stanza(stanza.RequestAck)
# Only end the session when a </stream> element is sent,
# not just because the connection has died.
self.xmpp.end_session_on_disconnect = False
# Register the feature twice because it may be ordered two
# different ways: enabling after binding and resumption
# before binding.
@@ -131,6 +128,7 @@ class XEP_0198(BasePlugin):
self.xmpp.add_filter('in', self._handle_incoming)
self.xmpp.add_filter('out_sync', self._handle_outgoing)
self.xmpp.add_event_handler('disconnected', self.disconnected)
self.xmpp.add_event_handler('session_end', self.session_end)
def plugin_end(self):
@@ -139,6 +137,7 @@ class XEP_0198(BasePlugin):
self.xmpp.unregister_feature('sm', self.order)
self.xmpp.unregister_feature('sm', self.resume_order)
self.xmpp.del_event_handler('disconnected', self.disconnected)
self.xmpp.del_event_handler('session_end', self.session_end)
self.xmpp.del_filter('in', self._handle_incoming)
self.xmpp.del_filter('out_sync', self._handle_outgoing)
@@ -154,9 +153,19 @@ class XEP_0198(BasePlugin):
self.xmpp.remove_stanza(stanza.Ack)
self.xmpp.remove_stanza(stanza.RequestAck)
def disconnected(self, event):
"""Reset enabled state until we can resume/reenable."""
log.debug("disconnected, disabling SM")
self.xmpp.event('sm_disabled', event)
self.enabled_in = False
self.enabled_out = False
def session_end(self, event):
"""Reset stream management state."""
self.enabled = False
log.debug("session_end, disabling SM")
self.xmpp.event('sm_disabled', event)
self.enabled_in = False
self.enabled_out = False
self.unacked_queue.clear()
self.sm_id = None
self.handled = 0
@@ -171,6 +180,7 @@ class XEP_0198(BasePlugin):
def request_ack(self, e=None):
"""Request an ack from the server."""
log.debug("requesting ack")
req = stanza.RequestAck(self.xmpp)
self.xmpp.send_raw(str(req))
@@ -193,9 +203,7 @@ class XEP_0198(BasePlugin):
enable = stanza.Enable(self.xmpp)
enable['resume'] = self.allow_resume
enable.send()
self.enabled = True
self.handled = 0
self.unacked_queue.clear()
log.debug("enabling SM")
waiter = Waiter('enabled_or_failed',
MatchMany([
@@ -204,11 +212,11 @@ class XEP_0198(BasePlugin):
self.xmpp.register_handler(waiter)
result = await waiter.wait()
elif self.sm_id and self.allow_resume and 'bind' not in self.xmpp.features:
self.enabled = True
resume = stanza.Resume(self.xmpp)
resume['h'] = self.handled
resume['previd'] = self.sm_id
resume.send()
log.debug("resuming SM")
# Wait for a response before allowing stream feature processing
# to continue. The actual result processing will be done in the
@@ -231,7 +239,10 @@ class XEP_0198(BasePlugin):
self.xmpp.features.add('stream_management')
if stanza['id']:
self.sm_id = stanza['id']
self.enabled_in = True
self.handled = 0
self.xmpp.event('sm_enabled', stanza)
self.xmpp.end_session_on_disconnect = False
def _handle_resumed(self, stanza):
"""Finish resuming a stream by resending unacked stanzas.
@@ -239,10 +250,12 @@ class XEP_0198(BasePlugin):
Raises a :term:`session_resumed` event.
"""
self.xmpp.features.add('stream_management')
self.enabled_in = True
self._handle_ack(stanza)
for id, stanza in self.unacked_queue:
self.xmpp.send(stanza, use_filters=False)
self.xmpp.event('session_resumed', stanza)
self.xmpp.end_session_on_disconnect = False
def _handle_failed(self, stanza):
"""
@@ -252,7 +265,8 @@ class XEP_0198(BasePlugin):
Raises an :term:`sm_failed` event.
"""
self.enabled = False
self.enabled_in = False
self.enabled_out = False
self.unacked_queue.clear()
self.xmpp.event('sm_failed', stanza)
@@ -289,7 +303,7 @@ class XEP_0198(BasePlugin):
def _handle_incoming(self, stanza):
"""Increment the handled counter for each inbound stanza."""
if not self.enabled:
if not self.enabled_in:
return stanza
if isinstance(stanza, (Message, Presence, Iq)):
@@ -299,7 +313,13 @@ class XEP_0198(BasePlugin):
def _handle_outgoing(self, stanza):
"""Store outgoing stanzas in a queue to be acked."""
if not self.enabled:
from slixmpp.plugins.xep_0198 import stanza as st
if isinstance(stanza, (st.Enable, st.Resume)):
self.enabled_out = True
self.unacked_queue.clear()
log.debug("enabling outgoing SM: %s" % stanza)
if not self.enabled_out:
return stanza
if isinstance(stanza, (Message, Presence, Iq)):

View File

@@ -112,9 +112,9 @@ class XEP_0199(BasePlugin):
try:
rtt = await self.ping(self.xmpp.boundjid.host, timeout=self.timeout)
except IqTimeout:
log.debug("Did not receive ping back in time." + \
log.debug("Did not receive ping back in time. " + \
"Requesting Reconnect.")
self.xmpp.reconnect()
self.xmpp.reconnect(0.0, "Ping timeout after %ds" % self.timeout)
else:
log.debug('Keepalive RTT: %s' % rtt)

View File

@@ -123,5 +123,5 @@ class EntityTime(ElementBase):
if not isinstance(value, dt.datetime):
date = xep_0082.parse(value)
date = date.astimezone(tzutc())
value = xep_0082.format_datetime(date)[:-1]
value = xep_0082.format_datetime(date)
self._set_sub_text('utc', value)

View File

@@ -40,7 +40,7 @@ class XEP_0202(BasePlugin):
}
def plugin_init(self):
"""Start the XEP-0203 plugin."""
"""Start the XEP-0202 plugin."""
if not self.local_time:
def default_local_time(jid):
@@ -50,7 +50,7 @@ class XEP_0202(BasePlugin):
self.xmpp.register_handler(
Callback('Entity Time',
StanzaPath('iq/entity_time'),
StanzaPath('iq@type=get/entity_time'),
self._handle_time_request))
register_stanza_plugin(Iq, stanza.EntityTime)

View File

@@ -26,7 +26,7 @@ class XEP_0223(BasePlugin):
dependencies = {'xep_0163', 'xep_0060', 'xep_0004'}
profile = {'pubsub#persist_items': True,
'pubsub#send_last_published_item': 'never'}
'pubsub#access_model': 'whitelist'}
def configure(self, node, ifrom=None, callback=None, timeout=None):
"""

View File

@@ -31,11 +31,11 @@ class XEP_0279(BasePlugin):
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:sic:0')
def check_ip(self, ifrom=None, block=True, timeout=None, callback=None,
def check_ip(self, ifrom=None, timeout=None, callback=None,
timeout_callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['from'] = ifrom
iq.enable('ip_check')
return iq.send(block=block, timeout=timeout, callback=callback,
return iq.send(timeout=timeout, callback=callback,
timeout_callback=timeout_callback)

View File

@@ -3,16 +3,18 @@
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permissio
See the file LICENSE for copying permission
"""
import logging
import slixmpp
from datetime import datetime
from typing import Any, Dict, Callable, Optional, Awaitable
from slixmpp import JID
from slixmpp.stanza import Message, Iq
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream.handler import Collector
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream.matcher import MatchXMLMask
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0313 import stanza
@@ -41,8 +43,32 @@ class XEP_0313(BasePlugin):
register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set)
register_stanza_plugin(stanza.Fin, self.xmpp['xep_0059'].stanza.Set)
def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None,
timeout=None, callback=None, iterator=False, rsm=None):
def retrieve(
self,
jid: Optional[JID] = None,
start: Optional[datetime] = None,
end: Optional[datetime] = None,
with_jid: Optional[JID] = None,
ifrom: Optional[JID] = None,
reverse: bool = False,
timeout: int = None,
callback: Callable[[Iq], None] = None,
iterator: bool = False,
rsm: Optional[Dict[str, Any]] = None
) -> Awaitable:
"""
Send a MAM query and retrieve the results.
:param JID jid: Entity holding the MAM records
:param datetime start,end: MAM query temporal boundaries
:param JID with_jid: Filter results on this JID
:param JID ifrom: To change the from address of the query
:param bool reverse: Get the results in reverse order
:param int timeout: IQ timeout
:param func callback: Custom callback for handling results
:param bool iterator: Use RSM and iterate over a paginated query
:param dict rsm: RSM custom options
"""
iq = self.xmpp.Iq()
query_id = iq['id']
@@ -53,35 +79,48 @@ class XEP_0313(BasePlugin):
iq['mam']['start'] = start
iq['mam']['end'] = end
iq['mam']['with'] = with_jid
amount = 10
if rsm:
for key, value in rsm.items():
iq['mam']['rsm'][key] = str(value)
if key == 'max':
amount = value
cb_data = {}
def pre_cb(query):
stanza_mask = self.xmpp.Message()
stanza_mask.xml.remove(stanza_mask.xml.find('{urn:xmpp:sid:0}origin-id'))
del stanza_mask['id']
del stanza_mask['lang']
stanza_mask['from'] = jid
stanza_mask['mam_result']['queryid'] = query_id
xml_mask = str(stanza_mask)
def pre_cb(query: Iq) -> None:
stanza_mask['mam_result']['queryid'] = query['id']
xml_mask = str(stanza_mask)
query['mam']['queryid'] = query['id']
collector = Collector(
'MAM_Results_%s' % query_id,
StanzaPath('message/mam_result@queryid=%s' % query['id']))
MatchXMLMask(xml_mask))
self.xmpp.register_handler(collector)
cb_data['collector'] = collector
def post_cb(result):
def post_cb(result: Iq) -> None:
results = cb_data['collector'].stop()
if result['type'] == 'result':
result['mam']['results'] = results
if iterator:
return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results',
recv_interface='mam_fin',
return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results', amount=amount,
reverse=reverse, recv_interface='mam_fin',
pre_cb=pre_cb, post_cb=post_cb)
collector = Collector(
'MAM_Results_%s' % query_id,
StanzaPath('message/mam_result@queryid=%s' % query_id))
MatchXMLMask(xml_mask))
self.xmpp.register_handler(collector)
def wrapped_cb(iq):
def wrapped_cb(iq: Iq) -> None:
results = collector.stop()
if iq['type'] == 'result':
iq['mam']['results'] = results
@@ -90,8 +129,15 @@ class XEP_0313(BasePlugin):
return iq.send(timeout=timeout, callback=wrapped_cb)
def get_preferences(self, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
query_id = iq['id']
iq['mam_prefs']['query_id'] = query_id
return iq.send(timeout=timeout, callback=callback)
def set_preferences(self, jid=None, default=None, always=None, never=None,
ifrom=None, block=True, timeout=None, callback=None):
ifrom=None, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = jid
@@ -99,7 +145,7 @@ class XEP_0313(BasePlugin):
iq['mam_prefs']['default'] = default
iq['mam_prefs']['always'] = always
iq['mam_prefs']['never'] = never
return iq.send(block=block, timeout=timeout, callback=callback)
return iq.send(timeout=timeout, callback=callback)
def get_configuration_commands(self, jid, **kwargs):
return self.xmpp['xep_0030'].get_items(

View File

@@ -516,7 +516,7 @@ class Field(ElementBase):
:param value: string
"""
pattern = re.compile("^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$")
pattern = re.compile(r"^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$")
if pattern.match(value) is not None:
self.xml.stringIds = value
else:

View File

@@ -114,7 +114,6 @@ class XEP_0332(BasePlugin):
iq['http-req']['data'] = data
return iq.send(
timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None)
)
@@ -135,7 +134,6 @@ class XEP_0332(BasePlugin):
iq['http-resp']['data'] = data
return iq.send(
timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None)
)
@@ -153,7 +151,6 @@ class XEP_0332(BasePlugin):
iq['id'] = kwargs["id"]
return iq.send(
timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None)
)

View File

@@ -0,0 +1,15 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2018 Maxime “pep” Buquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0335 import stanza
from slixmpp.plugins.xep_0335.stanza import JSON_Container
from slixmpp.plugins.xep_0335.json_containers import XEP_0335
register_plugin(XEP_0335)

View File

@@ -0,0 +1,23 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2018 Maxime “pep” Buquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp import Message
from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins.xep_0335 import JSON_Container
from slixmpp.plugins.xep_0335 import stanza
class XEP_0335(BasePlugin):
name = 'xep_0335'
description = 'XEP-0335: JSON Containers'
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Message, JSON_Container)

View File

@@ -0,0 +1,28 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2018 Maxime “pep” Buquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import json
from slixmpp.xmlstream import ElementBase
class JSON_Container(ElementBase):
name = 'json'
plugin_attrib = 'json'
namespace = 'urn:xmpp:json:0'
interfaces = {'value'}
def get_value(self):
return json.loads(self.xml.text)
def set_value(self, value):
if not isinstance(value, str):
value = json.dumps(value)
self.xml.text = value
def del_value(self):
self.xml.text = ''

View File

@@ -28,7 +28,13 @@ class UploadServiceNotFound(FileUploadError):
pass
class FileTooBig(FileUploadError):
pass
def __str__(self):
return 'File size too large: {} (max: {} bytes)' \
.format(self.args[0], self.args[1])
class HTTPError(FileUploadError):
def __str__(self):
return 'Could not upload file: %d (%s)' % (self.args[0], self.args[1])
class XEP_0363(BasePlugin):
''' This plugin only supports Python 3.5+ '''
@@ -112,7 +118,7 @@ class XEP_0363(BasePlugin):
except (TypeError, ValueError):
log.error('Invalid max size received from HTTP File Upload service')
self.max_file_size = float('+inf')
break
break
if input_file is None:
input_file = open(filename, 'rb')
@@ -122,7 +128,7 @@ class XEP_0363(BasePlugin):
input_file.seek(0)
if size > self.max_file_size:
raise FileTooBig()
raise FileTooBig(size, self.max_file_size)
if content_type is None:
content_type = guess_type(filename)[0]
@@ -148,6 +154,8 @@ class XEP_0363(BasePlugin):
data=input_file,
headers=headers,
timeout=timeout)
if response.status >= 400:
raise HTTPError(response.status, await response.text())
log.info('Response code: %d (%s)', response.status, await response.text())
response.close()
return slot['get']['url']

View File

@@ -49,15 +49,17 @@ class XEP_0380(BasePlugin):
register_stanza_plugin(Message, Encryption)
def plugin_end(self):
self.xmpp.remove_handler('Chat State')
def session_bind(self, jid):
self.xmpp.plugin['xep_0030'].add_feature(Encryption.namespace)
def has_eme(self, msg):
return msg.xml.find('{%s}encryption' % Encryption.namespace) is not None
def add_eme(self, msg: Message, namespace: str) -> Message:
msg['eme']['name'] = self.mechanisms[namespace]
msg['eme']['namespace'] = namespace
return msg
def replace_body_with_eme(self, msg):
eme = msg['eme']
namespace = eme['namespace']

View File

@@ -187,6 +187,10 @@ class Iq(RootStanza):
future = asyncio.Future()
# Prevents handlers from existing forever.
if timeout is None:
timeout = 120
def callback_success(result):
type_ = result['type']
if type_ == 'result':

View File

@@ -10,6 +10,9 @@ from slixmpp.stanza.rootstanza import RootStanza
from slixmpp.xmlstream import StanzaBase, ET
ORIGIN_NAME = '{urn:xmpp:sid:0}origin-id'
class Message(RootStanza):
"""
@@ -63,6 +66,8 @@ class Message(RootStanza):
if self['id'] == '':
if self.stream is not None and self.stream.use_message_ids:
self['id'] = self.stream.new_id()
else:
del self['origin_id']
def get_type(self):
"""
@@ -76,6 +81,43 @@ class Message(RootStanza):
"""
return self._get_attr('type', 'normal')
def get_id(self):
return self._get_attr('id') or ''
def get_origin_id(self):
sub = self.xml.find(ORIGIN_NAME)
if sub is not None:
return sub.attrib.get('id') or ''
return ''
def _set_ids(self, value) -> None:
if value is None or value == '':
return None
self.xml.attrib['id'] = value
if not self.stream.use_origin_id:
return None
sub = self.xml.find(ORIGIN_NAME)
if sub is not None:
sub.attrib['id'] = value
else:
sub = ET.Element(ORIGIN_NAME)
sub.attrib['id'] = value
self.xml.append(sub)
def set_id(self, value):
return self._set_ids(value)
def set_origin_id(self, value: str):
return self._set_ids(value)
def del_origin_id(self):
sub = self.xml.find(ORIGIN_NAME)
if sub is not None:
self.xml.remove(sub)
def get_parent_thread(self):
"""Return the message thread's parent thread.
@@ -140,6 +182,8 @@ class Message(RootStanza):
new_message['parent_thread'] = self['parent_thread']
del new_message['id']
if self.stream is not None and self.stream.use_message_ids:
new_message['id'] = self.stream.new_id()
if body is not None:
new_message['body'] = body

View File

@@ -90,10 +90,10 @@ class Presence(RootStanza):
def get_type(self):
"""
Return the value of the <presence> stanza's type attribute, or
the value of the <show> element.
the value of the <show> element if valid.
"""
out = self._get_attr('type')
if not out:
if not out and self['show'] in self.showtypes:
out = self['show']
if not out or out is None:
out = 'available'

View File

@@ -340,11 +340,19 @@ class SlixTest(unittest.TestCase):
self.xmpp.default_lang = None
self.xmpp.peer_default_lang = None
def new_id():
self.xmpp._id += 1
return str(self.xmpp._id)
self.xmpp._id = 0
self.xmpp.new_id = new_id
# Must have the stream header ready for xmpp.process() to work.
if not header:
header = self.xmpp.stream_header
self.xmpp.data_received(header)
self.wait_for_send_queue()
if skip:
self.xmpp.socket.next_sent()
@@ -361,6 +369,7 @@ class SlixTest(unittest.TestCase):
# Some plugins require messages to have ID values. Set
# this to True in tests related to those plugins.
self.xmpp.use_message_ids = False
self.xmpp.use_presence_ids = False
def make_header(self, sto='',
sfrom='',
@@ -591,6 +600,7 @@ class SlixTest(unittest.TestCase):
'id', 'stanzapath', 'xpath', and 'mask'.
Defaults to the value of self.match_method.
"""
self.wait_for_send_queue()
sent = self.xmpp.socket.next_sent(timeout)
if data is None and sent is None:
return
@@ -607,6 +617,14 @@ class SlixTest(unittest.TestCase):
defaults=defaults,
use_values=use_values)
def wait_for_send_queue(self):
loop = asyncio.get_event_loop()
future = asyncio.ensure_future(self.xmpp.run_filters(), loop=loop)
queue = self.xmpp.waiting_queue
print(queue)
loop.run_until_complete(queue.join())
future.cancel()
def stream_close(self):
"""
Disconnect the dummy XMPP client.

View File

@@ -160,7 +160,7 @@ except:
return _fixed_offset_tzs[offsetmins]
_iso8601_parser = re.compile("""
_iso8601_parser = re.compile(r"""
^
(?P<year> [0-9]{4})?(?P<ymdsep>-?)?
(?P<month>[0-9]{2})?(?P=ymdsep)?

View File

@@ -18,6 +18,9 @@ class Cache:
def store(self, key, value):
raise NotImplementedError
def remove(self, key):
raise NotImplemented
class PerJidCache:
def retrieve_by_jid(self, jid, key):
raise NotImplementedError
@@ -25,6 +28,9 @@ class PerJidCache:
def store_by_jid(self, jid, key, value):
raise NotImplementedError
def remove_by_jid(self, jid, key):
raise NotImplementedError
class MemoryCache(Cache):
def __init__(self):
self.cache = {}
@@ -36,6 +42,11 @@ class MemoryCache(Cache):
self.cache[key] = value
return True
def remove(self, key):
if key in self.cache:
del self.cache[key]
return True
class MemoryPerJidCache(PerJidCache):
def __init__(self):
self.cache = {}
@@ -51,6 +62,12 @@ class MemoryPerJidCache(PerJidCache):
cache[key] = value
return True
def remove_by_jid(self, jid, key):
cache = self.cache.get(jid, None)
if cache is not None and key in cache:
del cache[key]
return True
class FileSystemStorage:
def __init__(self, encode, decode, binary):
self.encode = encode if encode is not None else lambda x: x
@@ -67,7 +84,10 @@ class FileSystemStorage:
log.debug('%s not present in cache', key)
except OSError:
log.debug('Failed to read %s from cache:', key, exc_info=True)
return None
except Exception:
log.debug('Failed to decode %s from cache:', key, exc_info=True)
log.debug('Removing %s entry', key)
self._remove(directory, key)
def _store(self, directory, key, value):
filename = os.path.join(directory, key.replace('/', '_'))
@@ -79,6 +99,17 @@ class FileSystemStorage:
except OSError:
log.debug('Failed to store %s to cache:', key, exc_info=True)
return False
except Exception:
log.debug('Failed to encode %s to cache:', key, exc_info=True)
def _remove(self, directory, key):
filename = os.path.join(directory, key.replace('/', '_'))
try:
os.remove(filename)
except OSError:
log.debug('Failed to remove %s from cache:', key, exc_info=True)
return False
return True
class FileSystemCache(Cache, FileSystemStorage):
def __init__(self, directory, cache_type, *, encode=None, decode=None, binary=False):
@@ -91,6 +122,9 @@ class FileSystemCache(Cache, FileSystemStorage):
def store(self, key, value):
return self._store(self.base_dir, key, value)
def remove(self, key):
return self._remove(self.base_dir, key)
class FileSystemPerJidCache(PerJidCache, FileSystemStorage):
def __init__(self, directory, cache_type, *, encode=None, decode=None, binary=False):
FileSystemStorage.__init__(self, encode, decode, binary)
@@ -103,3 +137,7 @@ class FileSystemPerJidCache(PerJidCache, FileSystemStorage):
def store_by_jid(self, jid, key, value):
directory = os.path.join(self.base_dir, jid)
return self._store(directory, key, value)
def remove_by_jid(self, jid, key):
directory = os.path.join(self.base_dir, jid)
return self._remove(directory, key)

View File

@@ -516,13 +516,13 @@ else:
def setup(self, name):
authzid = self.credentials['authzid']
if not authzid:
authzid = 'xmpp@%s' % self.credentials['service-name']
authzid = 'xmpp@' + self.credentials['service-name'].decode()
_, self.gss = kerberos.authGSSClientInit(authzid)
self.step = 0
def process(self, challenge=b''):
b64_challenge = b64encode(challenge)
b64_challenge = b64encode(challenge).decode('ascii')
try:
if self.step == 0:
result = kerberos.authGSSClientStep(self.gss, b64_challenge)
@@ -536,7 +536,7 @@ else:
kerberos.authGSSClientUnwrap(self.gss, b64_challenge)
resp = kerberos.authGSSClientResponse(self.gss)
kerberos.authGSSClientWrap(self.gss, resp, username)
kerberos.authGSSClientWrap(self.gss, resp, username.decode())
resp = kerberos.authGSSClientResponse(self.gss)
except kerberos.GSSError as e:

View File

@@ -9,5 +9,5 @@
# We don't want to have to import the entire library
# just to get the version info for setup.py
__version__ = '1.4.1'
__version_info__ = (1, 4, 1)
__version__ = '1.5.2'
__version_info__ = (1, 5, 2)

View File

@@ -17,7 +17,7 @@ from __future__ import with_statement, unicode_literals
import copy
import logging
import weakref
from xml.etree import cElementTree as ET
from xml.etree import ElementTree as ET
from slixmpp.xmlstream import JID
from slixmpp.xmlstream.tostring import tostring
@@ -177,8 +177,9 @@ def fix_ns(xpath, split=False, propagate_ns=True, default_ns=''):
if '}' in ns_block:
# Apply the found namespace to following elements
# that do not have namespaces.
namespace = ns_block.split('}')[0]
elements = ns_block.split('}')[1].split('/')
ns_block_split = ns_block.split('}')
namespace = ns_block_split[0]
elements = ns_block_split[1].split('/')
else:
# Apply the stanza's namespace to the following
# elements since no namespace was provided.
@@ -202,7 +203,7 @@ class ElementBase(object):
"""
The core of Slixmpp's stanza XML manipulation and handling is provided
by ElementBase. ElementBase wraps XML cElementTree objects and enables
by ElementBase. ElementBase wraps XML ElementTree objects and enables
access to the XML contents through dictionary syntax, similar in style
to the Ruby XMPP library Blather's stanza implementation.
@@ -386,7 +387,7 @@ class ElementBase(object):
self._index = 0
#: The underlying XML object for the stanza. It is a standard
#: :class:`xml.etree.cElementTree` object.
#: :class:`xml.etree.ElementTree` object.
self.xml = xml
#: An ordered dictionary of plugin stanzas, mapped by their
@@ -1030,14 +1031,19 @@ class ElementBase(object):
if not lang:
lang = default_lang
parent = self.xml
for level, _ in enumerate(path):
# Generate the paths to the target elements and their parent.
element_path = "/".join(path[:len(path) - level])
parent_path = "/".join(path[:len(path) - level - 1])
elements = self.xml.findall(element_path)
parent = self.xml.find(parent_path)
if parent_path == '':
parent_path = None
if parent_path is not None:
parent = self.xml.find(parent_path)
if elements:
if parent is None:
parent = self.xml
@@ -1373,14 +1379,6 @@ class StanzaBase(ElementBase):
#: The default XMPP client namespace
namespace = 'jabber:client'
#: There is a small set of attributes which apply to all XMPP stanzas:
#: the stanza type, the to and from JIDs, the stanza ID, and, especially
#: in the case of an Iq stanza, a payload.
interfaces = {'type', 'to', 'from', 'id', 'payload'}
#: A basic set of allowed values for the ``'type'`` interface.
types = {'get', 'set', 'error', None, 'unavailable', 'normal', 'chat'}
def __init__(self, stream=None, xml=None, stype=None,
sto=None, sfrom=None, sid=None, parent=None):
self.stream = stream

View File

@@ -45,11 +45,12 @@ def tostring(xml=None, xmlns='', stream=None, outbuffer='',
output = [outbuffer]
# Extract the element's tag name.
tag_name = xml.tag.split('}', 1)[-1]
tag_split = xml.tag.split('}', 1)
tag_name = tag_split[-1]
# Extract the element's namespace if it is defined.
if '}' in xml.tag:
tag_xmlns = xml.tag.split('}', 1)[0][1:]
tag_xmlns = tag_split[0][1:]
else:
tag_xmlns = ''
@@ -82,8 +83,9 @@ def tostring(xml=None, xmlns='', stream=None, outbuffer='',
if '}' not in attrib:
output.append(' %s="%s"' % (attrib, value))
else:
attrib_ns = attrib.split('}')[0][1:]
attrib = attrib.split('}')[1]
attrib_split = attrib.split('}')
attrib_ns = attrib_split[0][1:]
attrib = attrib_split[1]
if attrib_ns == XML_NS:
output.append(' xml:%s="%s"' % (attrib, value))
elif stream and attrib_ns in stream.namespace_map:
@@ -144,10 +146,7 @@ def escape(text, use_cdata=False):
'"': '&quot;'}
if not use_cdata:
text = list(text)
for i, c in enumerate(text):
text[i] = escapes.get(c, c)
return ''.join(text)
return ''.join(escapes.get(c, c) for c in text)
else:
escape_needed = False
for c in text:

View File

@@ -12,6 +12,8 @@
:license: MIT, see LICENSE for more details
"""
from typing import Optional, Set, Callable
import functools
import logging
import socket as Socket
@@ -19,6 +21,8 @@ import ssl
import weakref
import uuid
from asyncio import iscoroutinefunction, wait
import xml.etree.ElementTree as ET
from slixmpp.xmlstream.asyncio import asyncio
@@ -30,6 +34,10 @@ from slixmpp.xmlstream.resolver import resolve, default_resolver
RESPONSE_TIMEOUT = 30
log = logging.getLogger(__name__)
class ContinueQueue(Exception):
"""
Exception raised in the send queue to "continue" from within an inner loop
"""
class NotConnectedError(Exception):
"""
@@ -81,6 +89,8 @@ class XMLStream(asyncio.BaseProtocol):
self.force_starttls = None
self.disable_starttls = None
self.waiting_queue = asyncio.Queue()
# A dict of {name: handle}
self.scheduled_events = {}
@@ -199,11 +209,6 @@ class XMLStream(asyncio.BaseProtocol):
self.__event_handlers = {}
self.__filters = {'in': [], 'out': [], 'out_sync': []}
self._id = 0
#: We use an ID prefix to ensure that all ID values are unique.
self._id_prefix = '%s-' % uuid.uuid4()
# Current connection attempt (Future)
self._current_connection_attempt = None
@@ -215,6 +220,9 @@ class XMLStream(asyncio.BaseProtocol):
#: ``_xmpp-client._tcp`` service.
self.dns_service = None
#: The reason why we are disconnecting from the server
self.disconnect_reason = None
#: An asyncio Future being done when the stream is disconnected.
self.disconnected = asyncio.Future()
@@ -238,12 +246,7 @@ class XMLStream(asyncio.BaseProtocol):
ID values. Using this method ensures that all new ID values
are unique in this stream.
"""
self._id += 1
return self.get_id()
def get_id(self):
"""Return the current unique stream ID in hexadecimal form."""
return "%s%X" % (self._id_prefix, self._id)
return uuid.uuid4().hex
def connect(self, host='', port=0, use_ssl=False,
force_starttls=True, disable_starttls=False):
@@ -268,7 +271,13 @@ class XMLStream(asyncio.BaseProtocol):
localhost
"""
asyncio.ensure_future(
self.run_filters(),
loop=self.loop,
)
self.disconnect_reason = None
self.cancel_connection_attempt()
self.connect_loop_wait = 0
if host and port:
self.address = (host, int(port))
try:
@@ -293,6 +302,10 @@ class XMLStream(asyncio.BaseProtocol):
async def _connect_routine(self):
self.event_when_connected = "connected"
if self.connect_loop_wait > 0:
self.event('reconnect_delay', self.connect_loop_wait)
await asyncio.sleep(self.connect_loop_wait, loop=self.loop)
record = await self.pick_dns_answer(self.default_domain)
if record is not None:
host, address, dns_port = record
@@ -309,7 +322,8 @@ class XMLStream(asyncio.BaseProtocol):
else:
ssl_context = None
await asyncio.sleep(self.connect_loop_wait, loop=self.loop)
if self._current_connection_attempt is None:
return
try:
await self.loop.create_connection(lambda: self,
self.address[0],
@@ -323,6 +337,8 @@ class XMLStream(asyncio.BaseProtocol):
except OSError as e:
log.debug('Connection failed: %s', e)
self.event("connection_failed", e)
if self._current_connection_attempt is None:
return
self.connect_loop_wait = self.connect_loop_wait * 2 + 1
self._current_connection_attempt = asyncio.ensure_future(
self._connect_routine(),
@@ -364,6 +380,7 @@ class XMLStream(asyncio.BaseProtocol):
"ssl_object",
default=self.transport.get_extra_info("socket")
)
self._current_connection_attempt = None
self.init_parser()
self.send_raw(self.stream_header)
self.dns_answers = None
@@ -398,7 +415,9 @@ class XMLStream(asyncio.BaseProtocol):
if self.xml_depth == 0:
# The stream's root element has closed,
# terminating the stream.
self.end_session_on_disconnect = True
log.debug("End of stream received")
self.disconnect_reason = "End of stream"
self.abort()
elif self.xml_depth == 1:
# A stanza is an XML element that is a direct child of
@@ -420,6 +439,9 @@ class XMLStream(asyncio.BaseProtocol):
self.send(error)
self.disconnect()
def is_connecting(self):
return self._current_connection_attempt is not None
def is_connected(self):
return self.transport is not None
@@ -433,7 +455,7 @@ class XMLStream(asyncio.BaseProtocol):
closure of the TCP connection
"""
log.info("connection_lost: %s", (exception,))
self.event("disconnected")
self.event("disconnected", self.disconnect_reason or exception and exception.strerror)
if self.end_session_on_disconnect:
self.event('session_end')
# All these objects are associated with one TCP connection. Since
@@ -453,24 +475,51 @@ class XMLStream(asyncio.BaseProtocol):
self._current_connection_attempt.cancel()
self._current_connection_attempt = None
def disconnect(self, wait=2.0):
def disconnect(self, wait: float = 2.0, reason: Optional[str] = None, ignore_send_queue: bool = False) -> None:
"""Close the XML stream and wait for an acknowldgement from the server for
at most `wait` seconds. After the given number of seconds has
passed without a response from the serveur, or when the server
passed without a response from the server, or when the server
successfully responds with a closure of its own stream, abort() is
called. If wait is 0.0, this is almost equivalent to calling abort()
directly.
called. If wait is 0.0, this will call abort() directly without closing
the stream.
Does nothing if we are not connected.
:param wait: Time to wait for a response from the server.
"""
self.cancel_connection_attempt()
# Compat: docs/getting_started/sendlogout.rst has been promoting
# `disconnect(wait=True)` for ages. This doesn't mean anything to the
# schedule call below. It would fortunately be converted to `1` later
# down the call chain. Praise the implicit casts lord.
if wait == True:
wait = 2.0
if self.transport:
if self.waiting_queue.empty() or ignore_send_queue:
self.disconnect_reason = reason
self.cancel_connection_attempt()
if wait > 0.0:
self.send_raw(self.stream_footer)
self.schedule('Disconnect wait', wait,
self.abort, repeat=False)
else:
asyncio.ensure_future(
self._consume_send_queue_before_disconnecting(reason, wait),
loop=self.loop,
)
else:
self.event("disconnected", reason)
async def _consume_send_queue_before_disconnecting(self, reason: Optional[str], wait: float):
"""Wait until the send queue is empty before disconnecting"""
await self.waiting_queue.join()
self.disconnect_reason = reason
self.cancel_connection_attempt()
if wait > 0.0:
self.send_raw(self.stream_footer)
self.schedule('Disconnect wait', wait,
self.abort, repeat=False)
self.schedule('Disconnect wait', wait,
self.abort, repeat=False)
def abort(self):
"""
@@ -483,14 +532,15 @@ class XMLStream(asyncio.BaseProtocol):
self.event("killed")
self.disconnected.set_result(True)
self.disconnected = asyncio.Future()
self.event("disconnected", self.disconnect_reason)
def reconnect(self, wait=2.0):
def reconnect(self, wait=2.0, reason="Reconnecting"):
"""Calls disconnect(), and once we are disconnected (after the timeout, or
when the server acknowledgement is received), call connect()
"""
log.debug("reconnecting...")
self.disconnect(wait)
self.add_event_handler('disconnected', self.connect, disposable=True)
self.add_event_handler('disconnected', lambda event: self.connect(), disposable=True)
self.disconnect(wait, reason)
def configure_socket(self):
"""Set timeout and other options for self.socket.
@@ -778,7 +828,7 @@ class XMLStream(asyncio.BaseProtocol):
# If the callback is a coroutine, schedule it instead of
# running it directly
if asyncio.iscoroutinefunction(handler_callback):
if iscoroutinefunction(handler_callback):
async def handler_callback_routine(cb):
try:
await cb(data)
@@ -865,7 +915,9 @@ class XMLStream(asyncio.BaseProtocol):
Execute the callback and remove the handler for it.
"""
self._safe_cb_run(name, cb)
del self.scheduled_events[name]
# workaround for specific events which unschedule themselves
if name in self.scheduled_events:
del self.scheduled_events[name]
def incoming_filter(self, xml):
"""Filter incoming XML objects before they are processed.
@@ -877,11 +929,93 @@ class XMLStream(asyncio.BaseProtocol):
"""
return xml
async def _continue_slow_send(
self,
task: asyncio.Task,
already_used: Set[Callable[[ElementBase], Optional[StanzaBase]]]
) -> None:
"""
Used when an item in the send queue has taken too long to process.
This is away from the send queue and can take as much time as needed.
:param asyncio.Task task: the Task wrapping the coroutine
:param set already_used: Filters already used on this outgoing stanza
"""
data = await task
for filter in self.__filters['out']:
if filter in already_used:
continue
if iscoroutinefunction(filter):
data = await task
else:
data = filter(data)
if data is None:
return
if isinstance(data, ElementBase):
for filter in self.__filters['out_sync']:
data = filter(data)
if data is None:
return
str_data = tostring(data.xml, xmlns=self.default_ns,
stream=self, top_level=True)
self.send_raw(str_data)
else:
self.send_raw(data)
async def run_filters(self):
"""
Background loop that processes stanzas to send.
"""
while True:
(data, use_filters) = await self.waiting_queue.get()
try:
if isinstance(data, ElementBase):
if use_filters:
already_run_filters = set()
for filter in self.__filters['out']:
already_run_filters.add(filter)
if iscoroutinefunction(filter):
task = asyncio.create_task(filter(data))
completed, pending = await wait(
{task},
timeout=1,
)
if pending:
asyncio.ensure_future(
self._continue_slow_send(
task,
already_run_filters
)
)
raise Exception("Slow coro, rescheduling")
data = task.result()
else:
data = filter(data)
if data is None:
raise ContinueQueue('Empty stanza')
if isinstance(data, ElementBase):
if use_filters:
for filter in self.__filters['out_sync']:
data = filter(data)
if data is None:
raise ContinueQueue('Empty stanza')
str_data = tostring(data.xml, xmlns=self.default_ns,
stream=self, top_level=True)
self.send_raw(str_data)
else:
self.send_raw(data)
except ContinueQueue as exc:
log.debug('Stanza in send queue not sent: %s', exc)
except Exception:
log.error('Exception raised in send queue:', exc_info=True)
self.waiting_queue.task_done()
def send(self, data, use_filters=True):
"""A wrapper for :meth:`send_raw()` for sending stanza objects.
May optionally block until an expected response is received.
:param data: The :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
stanza to send on the stream.
:param bool use_filters: Indicates if outgoing filters should be
@@ -889,24 +1023,7 @@ class XMLStream(asyncio.BaseProtocol):
filters is useful when resending stanzas.
Defaults to ``True``.
"""
if isinstance(data, ElementBase):
if use_filters:
for filter in self.__filters['out']:
data = filter(data)
if data is None:
return
if isinstance(data, ElementBase):
if use_filters:
for filter in self.__filters['out_sync']:
data = filter(data)
if data is None:
return
str_data = tostring(data.xml, xmlns=self.default_ns,
stream=self, top_level=True)
self.send_raw(str_data)
else:
self.send_raw(data)
self.waiting_queue.put_nowait((data, use_filters))
def send_xml(self, data):
"""Send an XML object on the stream

74
tests/test_cache.py Normal file
View File

@@ -0,0 +1,74 @@
import unittest
from slixmpp.test import SlixTest
from slixmpp.util import (
MemoryCache, MemoryPerJidCache,
FileSystemCache, FileSystemPerJidCache
)
from tempfile import TemporaryDirectory
class TestCacheClass(SlixTest):
def testMemoryCache(self):
cache = MemoryCache()
cache.store("test", "test_value")
self.assertEqual(cache.retrieve("test"), "test_value")
self.assertEqual(cache.retrieve("test2"), None)
cache.remove("test")
self.assertEqual(cache.retrieve("test"), None)
def testMemoryPerJidcache(self):
cache = MemoryPerJidCache()
cache.store_by_jid("test@example.com", "test", "test_value")
self.assertEqual(
cache.retrieve_by_jid("test@example.com", "test"),
"test_value"
)
cache.remove_by_jid("test@example.com", "test")
self.assertEqual(
cache.retrieve_by_jid("test@example.com", "test"),
None
)
def testFileSystemCache(self):
def failing_decode(value):
if value == "failme":
raise Exception("you failed")
return value
with TemporaryDirectory() as tmpdir:
cache = FileSystemCache(tmpdir, "test", decode=failing_decode)
cache.store("test", "test_value")
cache.store("test2", "failme")
self.assertEqual(
cache.retrieve("test"),
"test_value"
)
cache.remove("test")
self.assertEqual(
cache.retrieve("test"),
None
)
self.assertEqual(
cache.retrieve("test2"),
None
)
def testFileSystemPerJidCache(self):
with TemporaryDirectory() as tmpdir:
cache = FileSystemPerJidCache(tmpdir, "test")
cache.store_by_jid("test@example.com", "test", "test_value")
self.assertEqual(
cache.retrieve_by_jid("test@example.com", "test"),
"test_value"
)
cache.remove_by_jid("test@example.com", "test")
self.assertEqual(
cache.retrieve_by_jid("test@example.com", "test"),
None
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestCacheClass)

View File

@@ -68,13 +68,5 @@ class TestStanzaBase(SlixTest):
self.assertTrue(stanza['payload'] == [],
"Stanza reply did not empty stanza payload.")
def testError(self):
"""Test marking a stanza as an error."""
stanza = StanzaBase()
stanza['type'] = 'get'
stanza.error()
self.assertTrue(stanza['type'] == 'error',
"Stanza type is not 'error' after calling error()")
suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaBase)

View File

@@ -4,6 +4,7 @@ import sys
import datetime
import time
import threading
import unittest
import re
from slixmpp.test import *
@@ -11,6 +12,7 @@ from slixmpp.xmlstream import ElementBase
from slixmpp.plugins.xep_0323.device import Device
@unittest.skip('')
class TestStreamSensorData(SlixTest):
"""