Compare commits

..

110 Commits

Author SHA1 Message Date
Emmanuel Gil Peyrot
292f3206f6 Skip tests with known missing idna validation 2024-04-19 14:37:19 +02:00
Emmanuel Gil Peyrot
d1f2e196db Initial Rust version. 2024-04-19 14:30:50 +02:00
Emmanuel Gil Peyrot
f084ad2724 Remove UnescapedJID
It hadn’t been functional for many years, producing invalid JIDs and
being confusing for users anyway.  Better remove it.
2024-04-19 13:57:29 +02:00
mathieui
7c79f28587 XEP-0199: handle component case for keepalive ping 2024-03-22 20:48:36 +01:00
mathieui
dcaf812a28 ci: build cython module for itests 2024-02-09 23:28:15 +01:00
mathieui
ae4de043d2 itests: fix default server call 2024-02-09 23:11:29 +01:00
mathieui
998bbb80ad itests: hardcode default MUC server 2024-02-09 23:07:32 +01:00
mathieui
5a5b36ab39 xmlstream: make mypy even happier 2024-02-09 22:58:20 +01:00
mathieui
f151f0a7ab xmlstream/componentxmpp: fix some typing issues
Make mypy happier
2024-02-09 22:55:20 +01:00
mathieui
2424a3b36f slixtest: cleanup loop only if needed
if not, get_event_loop will throw, we can ignore this
2024-02-09 22:49:47 +01:00
mathieui
1c4bbbce8e ci: fix mypy step 2024-02-09 21:41:03 +01:00
mathieui
66d552d057 xep_0317: Fix compatibility with python < 3.9 2024-02-09 21:32:19 +01:00
nicoco
b8205a9ae4 Update plugin: XEP-0317 (hats)
Merge changes from nicoco's MR that I missed, improving tests and
interface.
2024-02-09 21:06:14 +01:00
nicoco
85b7210115 XEP-0264: Jingle Content Thumbnails (new plugin)
Cheogram actually uses it with SIMS to embed
a blurhash preview in the stanza.
2024-02-09 12:10:12 +01:00
nicoco
909c865524 XEP-0313: Do not try to parse date for fields without value.
Without this we end up passing "None"
instead of a str to the date parser,
which raises a TypeError.
It happens if you try to provide a form
to be filled, when slixmpp acts as a MAM
*server*.
2024-02-09 11:51:34 +01:00
nicoco
586d2f5107 XEP-0313: Add support for flipped page 2024-02-08 20:45:48 +01:00
nicoco
9f7260747f Add XEP_0461 to PluginDict 2024-02-08 20:34:16 +01:00
mathieui
c41209510a xep_049: implement bookmarks pinning stanzas 2024-02-04 11:59:36 +01:00
mathieui
9266486f46 xep_0317: add initial stanza support for hats 2024-02-04 11:32:24 +01:00
mathieui
5226858e0c Release 1.8.5 2024-02-02 01:59:31 +01:00
mathieui
7128ea249b Fix running process() with a timeout (closes #3505) 2024-02-02 01:00:25 +01:00
Maxime “pep” Buquet
992d80dd09 SCRAM: Restrict tls-unique to TLSv1.2
And prepare the code to work when CPython implements tls-exporter for
TLSv1.3.
This adds tls_version and binding_proposed attributes to the security
properties so we can detect if we were offerred channel binding SASL
mechanisms, and which TLS version we are on.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2024-02-02 00:56:40 +01:00
mathieui
c25305e80f componentxmpp: fix default host for components 2023-12-29 14:13:41 +01:00
mathieui
6765f84133 tests: close event loop at exit
prevents a nice segfault
2023-12-29 13:53:58 +01:00
mathieui
31fe7f7e06 [CI] add woodpecker CI 2023-12-29 13:53:58 +01:00
nicoco
84a7ac020f XEP-0461: rely on XEP-0428 for fallback
Breaks the previous fallback helpers, we now
rely on XEP-0461 instead
2023-12-28 16:38:37 +00:00
nicoco
331c1c1e21 XEP-0428: add fallback body and subject elements
+ tests
+ helpers to strip the fallback content
2023-12-28 16:38:37 +00:00
nicoco
28a60c22e2 ElementBase: add weak ref to parent when using append() 2023-12-28 16:38:37 +00:00
nicoco
af934b5bdf fix slixmpp.xmlstream.__all__ 2023-12-28 16:38:37 +00:00
genghis
897f876504 Correct Slixfeed title and add groupchat link to Stable Diffusion 2023-12-28 16:04:19 +00:00
genghis
2888be17ab Correct groupchat link for WhisperBot 2023-12-28 16:04:19 +00:00
genghis
975e31229c Correct links so they match to their respective text 2023-12-28 16:04:19 +00:00
genghis
6e9e66139d Add Stable Diffusion 2023-12-28 16:04:19 +00:00
genghis
380ac04d52 Update docs/projects.rst 2023-12-28 16:04:19 +00:00
genghis
9e5b530607 Update docs/projects.rst 2023-12-28 16:04:19 +00:00
genghis
71de274fab Update docs/projects.rst 2023-12-28 16:04:19 +00:00
genghis
5a0b02378d Add document Projects
Bots and Services utilizing Slixmpp
2023-12-28 16:04:19 +00:00
sxavier
9fc82e9e6f xep_0221: Add documentation overview and example 2023-12-28 16:01:19 +00:00
nicoco
ca90d3908e xep-0115: perf: avoid simultaneous disco info queries for the same verstring 2023-12-28 15:56:44 +00:00
Daniel Roschka
7de5cbcf33 Fix connect parameters used for follow-up calls
XMLStream.connect() is supposed to persist the parameters
it gets called with to allow follow-up calls to call
XMLStream.connect() without any parameters to result in a connection
with the same properties as the original one. That's for example used by
XMLStream.reconnect() when establishing a new connection.

Unfortunately that was broken for some of the parameters and resulted
different TLS related settings on reconnections. This commit fixes that.
2023-12-27 11:45:04 +01:00
Nicolas Cedilnik
76a11d4899 xep0356: implement IQ privilege
Also included:

- correctly handle privileges from different
  servers
- check that privileges have been granted before
  attempting to send something and raise
  PermissionError if not
- use dataclass and enums to store permissions instead of
  untyped dict
2023-12-19 14:14:16 +00:00
mathieui
dcfa0f20f9 [docs] add readthedocs.yaml 2023-11-13 19:38:48 +01:00
mathieui
7732af8991 Move references from lab.louiz.org to codeberg 2023-07-06 15:26:57 +02:00
nicoco
25c28ff5d1 xep_0461/add_quoted_fallback: add optional nickname argument
+ a little docstring that doesn't hurt
2023-06-05 20:48:38 +02:00
nicoco
e3e0d8f43e xep_0313/fin: add 'stable' and 'complete' attribs 2023-06-05 14:18:07 +02:00
nicoco
13729e47a6 add xeps 0385 and 0447 to plugins.PLUGINS 2023-06-05 14:18:07 +02:00
nicoco
f12860bfad fix misleading error msg
plugins.__all__ became plugins.PLUGINS a few commits ago
2023-06-05 14:18:07 +02:00
mathieui
bcbc7281e7 Release 1.8.4 2023-05-28 12:59:22 +02:00
mathieui
8787aa1064 Merge branch 'fix_error_ns_for_components_for_real' into 'master'
ComponentXMPP: fix fix_error_ns option

See merge request poezio/slixmpp!247
2023-05-11 19:08:22 +00:00
nicoco
f3522eb84b ComponentXMPP: fix fix_error_ns option
self.default_ns is defined in BaseXMPP.__init__(),
so ComponentXMPP._fix_error_ns() has to be called
later
2023-05-11 21:06:11 +02:00
Link Mauve
da9646cdaa Merge branch 'fix-register' into 'master'
Fix registration

See merge request poezio/slixmpp!246
2023-05-01 18:04:20 +00:00
Emmanuel Gil Peyrot
db1fc5fbc5 xmlstream: Fix registration
This iq nonza wasn’t marked as allowed to be sent on an unauthenticated
stream.
2023-05-01 19:50:26 +02:00
mathieui
209554e63f Merge branch 'ci-and-python-3.7' into 'master'
Fix CI issues and python 3.7 compatibility

See merge request poezio/slixmpp!244
2023-04-18 17:43:21 +00:00
mathieui
2d02ef9bcb exceptions: Fix python 3.7 compatibility 2023-04-18 19:41:02 +02:00
mathieui
18c3db4d6e ci: update python images 2023-04-18 19:41:02 +02:00
Nicoco K
6d6fdc6419 Merge branch 'fix-test-0377' into 'master'
Fix test for 0377 and update DOAP

See merge request poezio/slixmpp!243
2023-04-04 10:49:29 +00:00
Emmanuel Gil Peyrot
4936fb06bf XEP-0377: Update the DOAP 2023-04-04 12:45:53 +02:00
Emmanuel Gil Peyrot
5e47286445 XEP-0377: Update tests against the latest version 2023-04-04 12:45:53 +02:00
Link Mauve
8bead23799 Merge branch 'xep402-extensions' into 'master'
xep0402: add password and extension

See merge request poezio/slixmpp!242
2023-04-04 10:05:01 +00:00
nicoco
56c906f207 xep0402: add password and extension 2023-04-04 09:20:50 +02:00
Maxime Buquet
876c82037f Merge branch 'jid-barejid' into 'master'
jid: add 'bare' parameter to JID to discard resource

See merge request poezio/slixmpp!238
2023-04-03 11:18:03 +00:00
Link Mauve
fae4a38e84 Merge branch '377-nsbump' into 'master'
XEP-0377: Update to latest revision (bump ns)

See merge request poezio/slixmpp!239
2023-04-03 11:09:43 +00:00
Maxime “pep” Buquet
2b59d299a1 XEP-0377: Update to latest revision
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2023-04-03 13:01:45 +02:00
Maxime Buquet
51a4efb0f4 Merge branch 'xep402' into 'master'
Add support for XEP0402 (PEP Native bookmarks)

See merge request poezio/slixmpp!240
2023-04-03 09:23:25 +00:00
Nicolas Cedilnik
8f77bd4ee5 Add support for XEP0402 (PEP Native bookmarks) 2023-04-03 05:47:46 +02:00
Maxime “pep” Buquet
71128349a4 jid: add 'bare' parameter to JID to discard resource
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2023-03-18 20:28:54 +01:00
Maxime Buquet
bc2cebae6c Merge branch 'disco-tasks' into 'master'
xep_0030: asyncio.wait takes tasks

See merge request poezio/slixmpp!228
2023-03-07 19:19:10 +00:00
mathieui
2080d08d63 Merge branch 'component-errors' into 'master'
Fix error namespace for components

Closes #3476 and #3474

See merge request poezio/slixmpp!237
2023-03-07 19:12:32 +00:00
nicoco
e16f72d32d fix error namespace for ComponentXMPP
in SlixTest, if mode=="component", restore
jabber:client namespace afterwards

Fixes #3476
Fixes #3474
2023-03-07 11:04:35 +01:00
Maxime Buquet
4fa068da54 Merge branch 'xep-0030-component' into 'master'
xep_0030: small fixes for components

See merge request poezio/slixmpp!236
2023-02-25 10:47:29 +00:00
nicoco
21e5cd4435 xep_0030: get_items(): fix ifrom for local calls
kwargs can never have 'ifrom' since it's in the
method signature
2023-02-25 10:29:21 +01:00
nicoco
1a40699bcc xep_0030: do not send IQ without 'from' attr when component 2023-02-25 10:27:56 +01:00
Maxime Buquet
ebb8bd1e71 Merge branch 'better-errors' into 'master'
Improve errors

See merge request poezio/slixmpp!230
2023-02-24 14:41:09 +00:00
nicoco
78b42bdbbe Message.reply(): do not use bare jid if component
Components are likely to be MUC services more than
muc participants, and reply() is used when XMPPError
is raised, where it makes no sense to strip the resource
2023-02-24 15:29:05 +01:00
nicoco
abd3f40e96 XMPPError: add optional by attribute
so it is added to the reply stanzas
2023-02-24 15:29:05 +01:00
nicoco
b6f148e4e6 errors: make error types and conditions Literals
and set recommended defaults for type based on condition
2023-02-24 15:29:05 +01:00
Maxime Buquet
968fb0bac3 Merge branch 'xep0447' into 'master'
XEP-0447: minimal support (outgoing)

See merge request poezio/slixmpp!235
2023-02-24 14:12:45 +00:00
Maxime Buquet
8dcbcbf8a0 Merge branch 'xep0385' into 'master'
XEP-0385: minimal support

See merge request poezio/slixmpp!234
2023-02-24 14:12:38 +00:00
Maxime Buquet
de7b2d33a3 Merge branch 'xep-0461-fixes' into 'master'
XEP-0461: fixes

See merge request poezio/slixmpp!233
2023-02-23 23:42:19 +00:00
Maxime Buquet
fd1af054c5 Merge branch 'xep-0054-fix-no-vcard' into 'master'
xep_0054: raise item-not-found instead of trying to call None.send()

See merge request poezio/slixmpp!232
2023-02-23 23:41:47 +00:00
Maxime Buquet
e34fbfb28f Merge branch 'adhoc-partial-coroutine' into 'master'
xep_0050:allow partial coroutines as handlers

See merge request poezio/slixmpp!231
2023-02-23 23:40:42 +00:00
nicoco
af16832ad0 XEP-0447: minimal support (outgoing) 2023-02-24 00:22:42 +01:00
nicoco
40a857de65 XEP-0461: fix char counting
I think this time I got it right, confirmed
against client implementations (dino and movim)
2023-02-24 00:15:34 +01:00
nicoco
79ffa1668f XEP-0461: fix fallback namespace
the XEP should be updated soon, confirmed by author,
other implementations use this namespace
2023-02-24 00:15:34 +01:00
nicoco
b4b1efe058 XEP-0385: minimal support
- includes bits of other required XEPs
- only implements 'outgoing' SIMS
2023-02-23 23:49:13 +01:00
nicoco
de358464d0 xep_0054: raise item-not-found instead of trying to call None.send() 2023-02-23 23:37:31 +01:00
nicoco
92b4f2a7eb xep_0050:allow partial coroutines as handlers 2023-02-23 23:33:13 +01:00
Maxime Buquet
1f934d375c Merge branch 'fix-tests' into 'master'
Fix tests and stop misusing __all__

See merge request poezio/slixmpp!229
2023-02-23 17:04:13 +00:00
nicoco
700ce6b32e xep0461: fix typo in test 2023-02-23 16:53:22 +01:00
nicoco
5efa9804ba xep0292: test: declare suite 2023-02-23 16:53:22 +01:00
nicoco
9b0be1ca2b stop misusing __all__ for default plugin list
this fixes tests by renaming the list of default plugins
from __all__ (which has a special meaning) to a separate
list called PLUGINS

no need to put BasePlugin in __all__ after all
if we don't use __all__ at all
2023-02-23 16:53:22 +01:00
Maxime “pep” Buquet
5c19f16287 xep_0030: asyncio.wait takes tasks
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2023-02-15 11:55:57 +01:00
Maxime Buquet
af07864cbb Merge branch 'fix-caps-fetch' into 'master'
XEP-0115: fix a missing await in caps fetching

See merge request poezio/slixmpp!227
2023-01-30 19:46:53 +00:00
mathieui
dc4b1c7367 XEP-0115: fix a missing await in caps fetching 2023-01-30 20:37:16 +01:00
j
4a6064772c xep_0027: Ensure data is a str before handling it
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2023-01-26 17:40:47 +01:00
Maxime “pep” Buquet
80a89061f1 xep_0045: Remove debug print. thanks kalkin
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2023-01-07 15:07:28 +01:00
Maxime “pep” Buquet
8f4d8f76d1 doap: fix 454 note entry
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2023-01-04 21:24:17 +01:00
Maxime Buquet
656248ede7 Merge branch 'xep-0292' into 'master'
implements XEP-0292 (vCard4 over XMPP)

See merge request poezio/slixmpp!221
2022-11-28 12:05:30 +00:00
Maxime Buquet
980afe791f Merge branch 'add-public-names-in-_all_' into 'master'
Add public names in  all

See merge request poezio/slixmpp!225
2022-11-28 12:04:28 +00:00
nicoco
3725177d0b add OptJidStr to types.__all__ 2022-11-28 13:03:05 +01:00
nicoco
26fb0d1f91 add BasePlugin to plugins.__all__ 2022-11-28 13:03:03 +01:00
Maxime Buquet
5eb17e7633 Merge branch 'xep0461-fallback-helper' into 'master'
XEP-0461 (replies) improvements

See merge request poezio/slixmpp!224
2022-11-28 11:59:00 +00:00
nicoco
fdca7d82c4 XEP-0461: fix character counting
Turns out we need to include the fallback/end code point,
unlike python slicing conventions
2022-11-28 07:15:26 +01:00
nicoco
9b89401b36 XEP-0461: add get fallback body helper 2022-11-22 10:23:52 +01:00
nicoco
7300f1285e XEP-0461: add to plugins.__all__ 2022-11-22 08:49:00 +01:00
nicoco
9b51be1e17 XEP-0461: add quoted fallback helper 2022-11-22 08:49:00 +01:00
nicoco
89b1e1e682 XEP-0461: use integers for fallback start/end 2022-11-22 08:45:04 +01:00
Maxime Buquet
a7501abe56 Merge branch 'xep0030-iqkwargs' into 'master'
xep_0030: allow extra args in get_info_from_domain

See merge request poezio/slixmpp!223
2022-11-15 10:13:49 +00:00
nicoco
6940e4276b xep_0030: allow extra args in get_info_from_domain 2022-11-15 09:23:50 +01:00
nicoco
65636b8cce implements XEP-0292 (vCard4 over XMPP) 2022-11-04 09:36:25 +01:00
123 changed files with 2760 additions and 790 deletions

7
.gitignore vendored
View File

@@ -14,4 +14,9 @@ slixmpp.egg-info/
.DS_STORE
.idea/
.vscode/
venv/
venv/
# Added by cargo
/target
/Cargo.lock

View File

@@ -12,14 +12,14 @@ mypy:
- pip3 install mypy
- mypy slixmpp
test:
test-3.7:
stage: test
tags:
- docker
image: ubuntu:latest
image: python:3.7
script:
- apt update
- apt install -y python3 python3-pip cython3 gpg
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
@@ -30,7 +30,7 @@ test-3.10:
image: python:3.10
script:
- apt update
- apt install -y python3 python3-pip cython3 gpg
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
@@ -38,11 +38,22 @@ test-3.11:
stage: test
tags:
- docker
image: python:3.11-rc
image: python:3.11
script:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
test-3.12:
stage: test
tags:
- docker
image: python:3.12-rc
allow_failure: true
script:
- apt update
- apt install -y python3 python3-pip cython3 gpg
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
@@ -50,14 +61,14 @@ test_integration:
stage: test
tags:
- docker
image: ubuntu:latest
image: python:3
only:
variables:
- $CI_ACCOUNT1
- $CI_ACCOUNT2
script:
- apt update
- apt install -y python3 python3-pip cython3 gpg
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp aiodns
- ./run_integration_tests.py

22
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,22 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# We recommend specifying your dependencies to enable reproducible builds:
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: docs/requirements.txt

6
.woodpecker/lint.yml Normal file
View File

@@ -0,0 +1,6 @@
steps:
mypy:
image: python:3
commands:
- pip3 install mypy types-setuptools
- mypy slixmpp

View File

@@ -0,0 +1,10 @@
steps:
test_integration:
image: "python:3.11"
secrets: [ci_account1, ci_account1_password, ci_account2, ci_account2_password, ci_muc_server]
commands:
- apt-get update
- apt-get install -y python3-pip cython3 gpg idn libidn-dev
- pip3 install emoji aiohttp aiodns
- python3 setup.py build_ext --inplace
- ./run_integration_tests.py

17
.woodpecker/test.yml Normal file
View File

@@ -0,0 +1,17 @@
steps:
unit_tests:
image: "python:${TAG}"
commands:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
matrix:
TAG:
- "3.7"
- "3.9"
- "3.8"
- "3.10"
- "3.11"
- "3.12"

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://lab.louiz.org/poezio/slixmpp/issues/new>`_
- a ticket `on the bug tracker <https://codeberg.org/poezio/slixmpp/issues/new>`_
- a pull request on github
- a simple message on `the XMPP MUC <xmpp:slixmpp@muc.poez.io>`_

13
Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "slixmpp"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
jid = "0.10"
pyo3 = "0.21"
[lib]
crate-type = ["cdylib"]

View File

@@ -8,13 +8,13 @@
<shortdesc xml:lang="en">Elegant Python library for XMPP</shortdesc>
<shortdesc xml:lang="fr">Bibliothèque pour XMPP élégante, en Python</shortdesc>
<homepage rdf:resource="https://lab.louiz.org/poezio/slixmpp/"/>
<download-page rdf:resource="https://lab.louiz.org/poezio/slixmpp/tags"/>
<bug-database rdf:resource="https://lab.louiz.org/poezio/slixmpp/issues"/>
<homepage rdf:resource="https://codeberg.org/poezio/slixmpp/"/>
<download-page rdf:resource="https://codeberg.org/poezio/slixmpp/tags"/>
<bug-database rdf:resource="https://codeberg.org/poezio/slixmpp/issues"/>
<developer-forum rdf:resource="xmpp:slixmpp@muc.poez.io?join"/>
<support-forum rdf:resource="xmpp:slixmpp@muc.poez.io?join"/>
<license rdf:resource="https://lab.louiz.org/poezio/slixmpp/blob/master/LICENSE"/>
<license rdf:resource="https://codeberg.org/poezio/slixmpp/raw/brach/master/LICENSE"/>
<language>en</language>
@@ -59,8 +59,8 @@
<repository>
<GitRepository>
<browse rdf:resource="https://lab.louiz.org/poezio/slixmpp"/>
<location rdf:resource="https://lab.louiz.org/poezio/slixmpp.git"/>
<browse rdf:resource="https://codeberg.org/poezio/slixmpp"/>
<location rdf:resource="https://codeberg.org/poezio/slixmpp.git"/>
</GitRepository>
</repository>
@@ -784,7 +784,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0377.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2</xmpp:version>
<xmpp:version>0.3</xmpp:version>
<xmpp:since>1.6.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
@@ -903,9 +903,10 @@
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0454.html"/>
<xmpp:status>no thumbnail support</xmpp:status>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
<xmpp:since>1.8.1</xmpp:since>
<xmpp:note>no thumbnail support</xmpp:note>
</xmpp:SupportedXep>
</implements>
@@ -1011,49 +1012,63 @@
<Version>
<revision>1.6.0</revision>
<created>2020-12-12</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.6.0/slixmpp-slix-1.6.0.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.6.0.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.7.0</revision>
<created>2021-01-29</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.7.0/slixmpp-slix-1.7.0.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.7.0.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.7.1</revision>
<created>2021-04-30</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.7.1/slixmpp-slix-1.7.1.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.7.1.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.0</revision>
<created>2022-02-27</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.0/slixmpp-slix-1.8.0.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.0.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.1</revision>
<created>2022-03-20</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.1/slixmpp-slix-1.8.1.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.1.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.2</revision>
<created>2022-04-06</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.2/slixmpp-slix-1.8.2.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.2.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.3</revision>
<created>2022-11-12</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.3/slixmpp-slix-1.8.3.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.3.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.4</revision>
<created>2023-05-28</created>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.4.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.5</revision>
<created>2024-02-02</created>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.5.tar.gz"/>
</Version>
</release>
</Project>

View File

@@ -0,0 +1,17 @@
XEP-0292: vCard4 Over XMPP
==========================
.. module:: slixmpp.plugins.xep_0292
.. autoclass:: XEP_0292
:members:
:exclude-members: plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0292.stanza
:members:
:undoc-members:

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 <https://lab.louiz.org/poezio/slixmpp>`_.
with `Git <https://codeberg.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 <https://lab.louiz.org/poezio/slixmpp>`_.
with `Git <https://codeberg.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
@@ -325,7 +325,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 <https://lab.louiz.org/poezio/slixmpp/tree/master/examples>`_.
can also be found in the Slixmpp `examples directory <https://codeberg.org/poezio/slixmpp/src/branch/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 <https://lab.louiz.org/poezio/slixmpp>`_.
from `Git <https://codeberg.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

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

95
docs/projects.rst Normal file
View File

@@ -0,0 +1,95 @@
Projects Using Slixmpp
======================
Applications
------------
sendxmpp-py
~~~~~~~~~~~
sendxmpp is a command line program and is the XMPP equivalent of sendmail. It is a Python version of the original sendxmpp which is written in Perl.
- `Source <https://github.com/moparisthebest/sendxmpp-py>`_
Bots
----
BotLogMauve
~~~~~~~~~~~
XMPP bot which logs groupchat messages. Logs are in text format, with one file per day and per groupchat.
- `Source <https://git.khaganat.net/khaganat/BotLogMauve>`_
LinkBot
~~~~~~~
This bot reveals the title of any shared link in a groupchat for quick content insight.
- `Source <https://git.xmpp-it.net/mario/XMPPBot>`_
llama-bot
~~~~~~~~~
Llama-bot enables engaging communication with the LLM (large language model) of llama.cpp, providing seamless and dynamic conversation with it.
- `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Source <https://github.com/decent-im/llama-bot>`_
- `Demo <xmpp:llama@decent.im?message>`_
Morbot
~~~~~~
Morbot is a simple Slixmpp bot that will take new articles from listed RSS feeds and send them to assigned XMPP MUCs.
- `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Source <https://codeberg.org/TheCoffeMaker/Morbot>`_
Slixfeed
~~~~~~~~
Slixfeed aims to be an easy to use and fully-featured news aggregator bot for XMPP. It provides a convenient access to Blogs, Fediverse and News websites along with filtering functionality.
- `Groupchat <xmpp:slixfeed@chat.woodpeckersnest.space?join>`_
- `Source <https://gitgud.io/sjehuda/slixfeed>`_
sms4you
~~~~~~~
sms4you forwards messages from and to SMS and connects either with sms4you-xmpp or sms4you-email to choose the other mean of communication. Nice for receiving or sending SMS, independently from carrying a SIM card.
- `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Homepage <https://sms4you-team.pages.debian.net/sms4you/>`_
- `Source <https://salsa.debian.org/sms4you-team/sms4you>`_
Stable Diffusion
~~~~~~~~~~~~~~~~
XMPP bot that generates digital images from textual descriptions.
- `Groupchat <xmpp:slidge@conference.nicoco.fr?join>`_
- `Source <https://www.nicoco.fr/blog/2022/08/31/xmpp-bot-stable-diffusion/>`_
WhisperBot
~~~~~~~~~~
XMPP bot that transliterates audio messages using OpenAI's Whisper libraries.
- `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Source <https://codeberg.org/TheCoffeMaker/WhisperBot>`_
XMPP MUC Message Gateway
~~~~~~~~~~~~~~~~~~~~~~~~
A multipurpose JSON forwarder microservice from HTTP POST to XMPP MUC room over TLSv1.2 with SliXMPP.
- `Source <https://github.com/immanuelfodor/xmpp-muc-message-gateway>`_
Services
--------
AtomToPubsub
~~~~~~~~~~~~
AtomToPubsub is a simple Python script that parses Atom + RSS feeds and pushes the entries to a designated XMPP Pubsub Node.
- `Groupchat <xmpp:movim@conference.movim.eu?join>`_
- `Source <https://github.com/imattau/atomtopubsub>`_
Slidge
~~~~~~
Slidge is a general purpose XMPP gateway framework in Python.
- `Groupchat <xmpp:slidge@conference.nicoco.fr?join>`_
- `Homepage <https://slidge.im/core/>`_
- `Source <https://sr.ht/~nicoco/slidge>`_

View File

@@ -10,7 +10,7 @@ UNIQUE = uuid4().hex
class TestMUC(SlixIntegration):
async def asyncSetUp(self):
self.mucserver = self.envjid('CI_MUC_SERVER')
self.mucserver = self.envjid('CI_MUC_SERVER', default='chat.jabberfr.org')
self.muc = JID('%s@%s' % (UNIQUE, self.mucserver))
self.add_client(
self.envjid('CI_ACCOUNT1'),

View File

@@ -80,7 +80,7 @@ setup(
long_description=LONG_DESCRIPTION,
author='Florent Le Coz',
author_email='louiz@louiz.org',
url='https://lab.louiz.org/poezio/slixmpp',
url='https://codeberg.org/poezio/slixmpp',
license='MIT',
platforms=['any'],
package_data={'slixmpp': ['py.typed']},

View File

@@ -279,13 +279,13 @@ class BaseXMPP(XMLStream):
if self.plugin_whitelist:
plugin_list = self.plugin_whitelist
else:
plugin_list = plugins.__all__
plugin_list = plugins.PLUGINS
for plugin in plugin_list:
if plugin in plugins.__all__:
if plugin in plugins.PLUGINS:
self.register_plugin(plugin)
else:
raise NameError("Plugin %s not in plugins.__all__." % plugin)
raise NameError("Plugin %s not in plugins.PLUGINS." % plugin)
def __getitem__(self, key):
"""Return a plugin given its name, if it has been registered."""

View File

@@ -138,8 +138,8 @@ class ClientXMPP(BaseXMPP):
self.credentials['password'] = value
def connect(self, address: Optional[Tuple[str, int]] = None, # type: ignore
use_ssl: bool = False, force_starttls: bool = True,
disable_starttls: bool = False) -> None:
use_ssl: Optional[bool] = None, force_starttls: Optional[bool] = None,
disable_starttls: Optional[bool] = None) -> None:
"""Connect to the XMPP server.
When no address is given, a SRV lookup for the server will
@@ -166,8 +166,8 @@ class ClientXMPP(BaseXMPP):
host, port = (self.boundjid.host, 5222)
self.dns_service = 'xmpp-client'
return XMLStream.connect(self, host, port, use_ssl=use_ssl,
force_starttls=force_starttls, disable_starttls=disable_starttls)
XMLStream.connect(self, host, port, use_ssl=use_ssl,
force_starttls=force_starttls, disable_starttls=disable_starttls)
def register_feature(self, name: str, handler: Callable, restart: bool = False, order: int = 5000) -> None:
"""Register a stream feature handler.

View File

@@ -9,13 +9,16 @@
import logging
import hashlib
from typing import Optional
from slixmpp import Message, Iq, Presence
from slixmpp.basexmpp import BaseXMPP
from slixmpp.stanza import Handshake
from slixmpp.stanza.error import Error
from slixmpp.xmlstream import XMLStream
from slixmpp.xmlstream import ET
from slixmpp.xmlstream.matcher import MatchXPath
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.stanzabase import register_stanza_plugin
log = logging.getLogger(__name__)
@@ -39,9 +42,17 @@ class ComponentXMPP(BaseXMPP):
should be used instead of the standard
``'jabber:component:accept'`` namespace.
Defaults to ``False``.
:param fix_error_ns: Fix the namespace of error stanzas.
If you use ``use_jc_ns`` namespace, you probably want that, but
it can be a problem if you use both a ClientXMPP and a ComponentXMPP
in the same interpreter. This is ``False`` by default for backwards
compatibility.
"""
def __init__(self, jid, secret, host=None, port=None, plugin_config=None, plugin_whitelist=None, use_jc_ns=False):
def __init__(self, jid, secret,
host=None, port=None, plugin_config=None,
plugin_whitelist=None, use_jc_ns=False,
fix_error_ns=False):
if not plugin_whitelist:
plugin_whitelist = []
@@ -53,6 +64,8 @@ class ComponentXMPP(BaseXMPP):
else:
default_ns = 'jabber:component:accept'
BaseXMPP.__init__(self, jid, default_ns)
if fix_error_ns:
self._fix_error_ns()
self.auto_authorize = None
self.stream_header = '<stream:stream %s %s to="%s">' % (
@@ -77,7 +90,14 @@ class ComponentXMPP(BaseXMPP):
self.add_event_handler('presence_probe',
self._handle_probe)
def connect(self, host=None, port=None, use_ssl=False):
def _fix_error_ns(self):
Error.namespace = self.default_ns
for st in Message, Iq, Presence:
register_stanza_plugin(st, Error)
def connect(self, host: Optional[str] = None, port: int = 0, use_ssl: Optional[bool] = None,
force_starttls: Optional[bool] = None,
disable_starttls: Optional[bool] = None) -> None:
"""Connect to the server.
@@ -87,17 +107,18 @@ class ComponentXMPP(BaseXMPP):
Defauts to :attr:`server_port`.
:param use_ssl: Flag indicating if SSL should be used by connecting
directly to a port using SSL.
:param force_starttls: UNUSED
:param disable_starttls: UNUSED
"""
if host is None:
host = self.server_host
if port is None:
port = self.server_port
if host is not None:
self.server_host = host
if port:
self.server_port = port
self.server_name = self.boundjid.host
log.debug("Connecting to %s:%s", host, port)
return XMLStream.connect(self, host=host, port=port,
use_ssl=use_ssl)
XMLStream.connect(self, host=self.server_host, port=self.server_port, use_ssl=use_ssl)
def incoming_filter(self, xml):
"""

View File

@@ -5,6 +5,11 @@
# :copyright: (c) 2011 Nathanael C. Fritz
# :license: MIT, see LICENSE for more details
from typing import Dict, Optional
from .types import ErrorConditions, ErrorTypes, JidStr
class XMPPError(Exception):
"""
@@ -37,12 +42,17 @@ class XMPPError(Exception):
Defaults to ``True``.
"""
def __init__(self, condition='undefined-condition', text='',
etype='cancel', extension=None, extension_ns=None,
extension_args=None, clear=True):
def __init__(self, condition: ErrorConditions='undefined-condition', text='',
etype: Optional[ErrorTypes]=None, extension=None, extension_ns=None,
extension_args=None, clear=True, by: Optional[JidStr] = None):
if extension_args is None:
extension_args = {}
if condition not in _DEFAULT_ERROR_TYPES:
raise ValueError("This is not a valid condition type", condition)
if etype is None:
etype = _DEFAULT_ERROR_TYPES[condition]
self.by = by
self.condition = condition
self.text = text
self.etype = etype
@@ -110,3 +120,29 @@ class PresenceError(XMPPError):
etype=pres['error']['type'],
)
self.presence = pres
_DEFAULT_ERROR_TYPES: Dict[ErrorConditions, ErrorTypes] = {
"bad-request": "modify",
"conflict": "cancel",
"feature-not-implemented": "cancel",
"forbidden": "auth",
"gone": "modify",
"internal-server-error": "wait",
"item-not-found": "cancel",
"jid-malformed": "modify",
"not-acceptable": "modify",
"not-allowed": "cancel",
"not-authorized": "auth",
"payment-required": "auth",
"recipient-unavailable": "wait",
"redirect": "modify",
"registration-required": "auth",
"remote-server-not-found": "cancel",
"remote-server-timeout": "wait",
"resource-constraint": "wait",
"service-unavailable": "cancel",
"subscription-required": "auth",
"undefined-condition": "cancel",
"unexpected-request": "modify",
}

View File

@@ -37,7 +37,8 @@ class FeatureMechanisms(BasePlugin):
'unencrypted_digest': False,
'unencrypted_cram': False,
'unencrypted_scram': True,
'order': 100
'order': 100,
'tls_version': None,
}
def plugin_init(self):
@@ -96,7 +97,20 @@ class FeatureMechanisms(BasePlugin):
result[value] = creds.get('email', jid)
elif value == 'channel_binding':
if isinstance(self.xmpp.socket, (ssl.SSLSocket, ssl.SSLObject)):
result[value] = self.xmpp.socket.get_channel_binding()
version = self.xmpp.socket.version()
# As of now, python does not implement anything else
# than tls-unique, which is forbidden on TLSv1.3
# see https://github.com/python/cpython/issues/95341
if version != 'TLSv1.3':
result[value] = self.xmpp.socket.get_channel_binding(
cb_type="tls-unique"
)
elif 'tls-exporter' in ssl.CHANNEL_BINDING_TYPES:
result[value] = self.xmpp.socket.get_channel_binding(
cb_type="tls-exporter"
)
else:
result[value] = None
else:
result[value] = None
elif value == 'host':
@@ -121,6 +135,11 @@ class FeatureMechanisms(BasePlugin):
result[value] = True
else:
result[value] = False
elif value == 'tls_version':
if isinstance(self.xmpp.socket, (ssl.SSLSocket, ssl.SSLObject)):
result[value] = self.xmpp.socket.version()
elif value == 'binding_proposed':
result[value] = any(x for x in self.mech_list if x.endswith('-PLUS'))
else:
result[value] = self.config.get(value, False)
return result

View File

@@ -1,440 +1 @@
# slixmpp.jid
# ~~~~~~~~~~~~~~~~~~~~~~~
# This module allows for working with Jabber IDs (JIDs).
# Part of Slixmpp: The Slick XMPP Library
# :copyright: (c) 2011 Nathanael C. Fritz
# :license: MIT, see LICENSE for more details
from __future__ import annotations
import re
import socket
from functools import lru_cache
from typing import (
Optional,
Union,
)
from slixmpp.stringprep import nodeprep, resourceprep, idna, StringprepError
HAVE_INET_PTON = hasattr(socket, 'inet_pton')
#: The basic regex pattern that a JID must match in order to determine
#: the local, domain, and resource parts. This regex does NOT do any
#: validation, which requires application of nodeprep, resourceprep, etc.
JID_PATTERN = re.compile(
"^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$"
)
#: The set of escape sequences for the characters not allowed by nodeprep.
JID_ESCAPE_SEQUENCES = {'\\20', '\\22', '\\26', '\\27', '\\2f',
'\\3a', '\\3c', '\\3e', '\\40', '\\5c'}
#: The reverse mapping of escape sequences to their original forms.
JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
'\\22': '"',
'\\26': '&',
'\\27': "'",
'\\2f': '/',
'\\3a': ':',
'\\3c': '<',
'\\3e': '>',
'\\40': '@',
'\\5c': '\\'}
# TODO: Find the best cache size for a standard usage.
@lru_cache(maxsize=1024)
def _parse_jid(data: str):
"""
Parse string data into the node, domain, and resource
components of a JID, if possible.
:param string data: A string that is potentially a JID.
:raises InvalidJID:
:returns: tuple of the validated local, domain, and resource strings
"""
match = JID_PATTERN.match(data)
if not match:
raise InvalidJID('JID could not be parsed')
(node, domain, resource) = match.groups()
node = _validate_node(node)
domain = _validate_domain(domain)
resource = _validate_resource(resource)
return node, domain, resource
def _validate_node(node: Optional[str]):
"""Validate the local, or username, portion of a JID.
:raises InvalidJID:
:returns: The local portion of a JID, as validated by nodeprep.
"""
if node is None:
return ''
try:
node = nodeprep(node)
except StringprepError:
raise InvalidJID('Nodeprep failed')
if not node:
raise InvalidJID('Localpart must not be 0 bytes')
if len(node) > 1023:
raise InvalidJID('Localpart must be less than 1024 bytes')
return node
def _validate_domain(domain: str):
"""Validate the domain portion of a JID.
IP literal addresses are left as-is, if valid. Domain names
are stripped of any trailing label separators (`.`), and are
checked with the nameprep profile of stringprep. If the given
domain is actually a punyencoded version of a domain name, it
is converted back into its original Unicode form. Domains must
also not start or end with a dash (`-`).
:raises InvalidJID:
:returns: The validated domain name
"""
ip_addr = False
# First, check if this is an IPv4 address
try:
socket.inet_aton(domain)
ip_addr = True
except socket.error:
pass
# Check if this is an IPv6 address
if not ip_addr and HAVE_INET_PTON and domain[0] == '[' and domain[-1] == ']':
try:
ip = domain[1:-1]
socket.inet_pton(socket.AF_INET6, ip)
ip_addr = True
except (socket.error, ValueError):
pass
if not ip_addr:
# This is a domain name, which must be checked further
if domain and domain[-1] == '.':
domain = domain[:-1]
try:
domain = idna(domain)
except StringprepError:
raise InvalidJID(f'idna validation failed: {domain}')
if ':' in domain:
raise InvalidJID(f'Domain containing a port: {domain}')
for label in domain.split('.'):
if not label:
raise InvalidJID(f'Domain containing too many dots: {domain}')
if '-' in (label[0], label[-1]):
raise InvalidJID(f'Domain starting or ending with -: {domain}')
if not domain:
raise InvalidJID('Domain must not be 0 bytes')
if len(domain) > 1023:
raise InvalidJID('Domain must be less than 1024 bytes')
return domain
def _validate_resource(resource: Optional[str]):
"""Validate the resource portion of a JID.
:raises InvalidJID:
:returns: The local portion of a JID, as validated by resourceprep.
"""
if resource is None:
return ''
try:
resource = resourceprep(resource)
except StringprepError:
raise InvalidJID('Resourceprep failed')
if not resource:
raise InvalidJID('Resource must not be 0 bytes')
if len(resource) > 1023:
raise InvalidJID('Resource must be less than 1024 bytes')
return resource
def _unescape_node(node: str):
"""Unescape a local portion of a JID.
.. note::
The unescaped local portion is meant ONLY for presentation,
and should not be used for other purposes.
"""
unescaped = []
seq = ''
for i, char in enumerate(node):
if char == '\\':
seq = node[i:i+3]
if seq not in JID_ESCAPE_SEQUENCES:
seq = ''
if seq:
if len(seq) == 3:
unescaped.append(JID_UNESCAPE_TRANSFORMATIONS.get(seq, char))
# Pop character off the escape sequence, and ignore it
seq = seq[1:]
else:
unescaped.append(char)
return ''.join(unescaped)
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.
:param string domain: Required. The domain name portion of the JID.
:param strin resource: Optional. The resource portion of the JID.
:return: A full or bare JID string.
"""
if domain is None:
return ''
if local is not None:
result = local + '@' + domain
else:
result = domain
if resource is not None:
result += '/' + resource
return result
class InvalidJID(ValueError):
"""
Raised when attempting to create a JID that does not pass validation.
It can also be raised if modifying an existing JID in such a way as
to make it invalid, such trying to remove the domain from an existing
full JID while the local and resource portions still exist.
"""
# pylint: disable=R0903
class UnescapedJID:
"""
.. versionadded:: 1.1.10
"""
__slots__ = ('_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: str):
"""Retrieve the given JID component.
:param name: one of: user, server, domain, resource,
full, or bare.
"""
if name == 'resource':
return self._resource or ''
if name in ('user', 'username', 'local', 'node'):
return self._node or ''
if name in ('server', 'domain', 'host'):
return self._domain or ''
if name in ('full', 'jid'):
return _format_jid(self._node, self._domain, self._resource)
if name == 'bare':
return _format_jid(self._node, self._domain)
return object.__getattribute__(self, name)
def __str__(self):
"""Use the full JID as the string value."""
return _format_jid(self._node, self._domain, self._resource)
def __repr__(self):
"""Use the full JID as the representation."""
return _format_jid(self._node, self._domain, self._resource)
class JID:
"""
A representation of a Jabber ID, or JID.
Each JID may have three components: a user, a domain, and an optional
resource. For example: user@domain/resource
When a resource is not used, the JID is called a bare JID.
The JID is a full JID otherwise.
**JID Properties:**
:full: The string value of the full JID.
:jid: Alias for ``full``.
:bare: The string value of the bare JID.
:node: The node portion of the JID.
:user: Alias for ``node``.
:local: Alias for ``node``.
:username: Alias for ``node``.
:domain: The domain name portion of the JID.
:server: Alias for ``domain``.
:host: Alias for ``domain``.
:resource: The resource portion of the JID.
:param string jid:
A string of the form ``'[user@]domain[/resource]'``.
:raises InvalidJID:
"""
__slots__ = ('_node', '_domain', '_resource', '_bare', '_full')
def __init__(self, jid: Optional[Union[str, 'JID']] = None):
if not jid:
self._node = ''
self._domain = ''
self._resource = ''
self._bare = ''
self._full = ''
return
elif not isinstance(jid, JID):
self._node, self._domain, self._resource = _parse_jid(jid)
else:
self._node = jid._node
self._domain = jid._domain
self._resource = jid._resource
self._update_bare_full()
def unescape(self):
"""Return an unescaped JID object.
Using an unescaped JID is preferred for displaying JIDs
to humans, and they should NOT be used for any other
purposes than for presentation.
:return: :class:`UnescapedJID`
.. versionadded:: 1.1.10
"""
return UnescapedJID(_unescape_node(self._node),
self._domain,
self._resource)
def _update_bare_full(self):
"""Format the given JID into a bare and a full JID.
"""
self._bare = (self._node + '@' + self._domain
if self._node
else self._domain)
self._full = (self._bare + '/' + self._resource
if self._resource
else self._bare)
@property
def bare(self) -> str:
return self._bare
@bare.setter
def bare(self, value: str):
node, domain, resource = _parse_jid(value)
assert not resource
self._node = node
self._domain = domain
self._update_bare_full()
@property
def node(self) -> str:
return self._node
@node.setter
def node(self, value: Optional[str]):
self._node = _validate_node(value)
self._update_bare_full()
@property
def domain(self) -> str:
return self._domain
@domain.setter
def domain(self, value: str):
self._domain = _validate_domain(value)
self._update_bare_full()
@property
def resource(self) -> str:
return self._resource
@resource.setter
def resource(self, value: Optional[str]):
self._resource = _validate_resource(value)
self._update_bare_full()
@property
def full(self) -> str:
return self._full
@full.setter
def full(self, value: str):
self._node, self._domain, self._resource = _parse_jid(value)
self._update_bare_full()
user = node
local = node
username = node
server = domain
host = domain
jid = full
def __str__(self):
"""Use the full JID as the string value."""
return self._full
def __repr__(self):
"""Use the full JID as the representation."""
return self._full
# pylint: disable=W0212
def __eq__(self, other):
"""Two JIDs are equal if they have the same full JID value."""
if isinstance(other, UnescapedJID):
return False
if not isinstance(other, JID):
try:
other = JID(other)
except InvalidJID:
return NotImplemented
return (self._node == other._node and
self._domain == other._domain and
self._resource == other._resource)
def __ne__(self, other):
"""Two JIDs are considered unequal if they are not equal."""
return not self == other
def __hash__(self):
"""Hash a JID based on the string version of its full JID."""
return hash(self._full)
from libslixmpp import JID, InvalidJID

View File

@@ -6,7 +6,7 @@ from slixmpp.plugins.base import PluginManager, PluginNotFound, BasePlugin
from slixmpp.plugins.base import register_plugin, load_plugin
__all__ = [
PLUGINS = [
# XEPS
'xep_0004', # Data Forms
'xep_0009', # Jabber-RPC
@@ -76,14 +76,17 @@ __all__ = [
'xep_0256', # Last Activity in Presence
'xep_0257', # Client Certificate Management for SASL EXTERNAL
'xep_0258', # Security Labels in XMPP
'xep_0264', # Jingle Content Thumbnails
# 'xep_0270', # XMPP Compliance Suites 2010. Dont automatically load
'xep_0279', # Server IP Check
'xep_0280', # Message Carbons
'xep_0292', # vCard4 Over XMPP
'xep_0297', # Stanza Forwarding
'xep_0300', # Use of Cryptographic Hash Functions in XMPP
# 'xep_0302', # XMPP Compliance Suites 2012. Dont automatically load
'xep_0308', # Last Message Correction
'xep_0313', # Message Archive Management
'xep_0317', # Hats
'xep_0319', # Last User Interaction in Presence
# 'xep_0323', # IoT Systems Sensor Data. Dont automatically load
# 'xep_0325', # IoT Systems Control. Dont automatically load
@@ -100,7 +103,9 @@ __all__ = [
'xep_0377', # Spam reporting
'xep_0380', # Explicit Message Encryption
'xep_0382', # Spoiler Messages
'xep_0385', # Stateless Inline Media Sharing (SIMS)
'xep_0394', # Message Markup
'xep_0402', # PEP Native Bookmarks
'xep_0403', # MIX-Presence
'xep_0404', # MIX-Anon
'xep_0405', # MIX-PAM
@@ -113,4 +118,16 @@ __all__ = [
'xep_0439', # Quick Response
'xep_0441', # Message Archive Management Preferences
'xep_0444', # Message Reactions
'xep_0447', # Stateless file sharing
'xep_0461', # Message Replies
'xep_0469', # Bookmarks Pinning
# Meant to be imported by plugins
]
__all__ = PLUGINS + [
'PluginManager',
'PluginNotFound',
'BasePlugin',
'register_plugin',
'load_plugin',
]

View File

@@ -19,6 +19,8 @@ def _extract_data(data, kind):
stripped = []
begin_headers = False
begin_data = False
if isinstance(data, bytes):
data = data.decode()
for line in data.split('\n'):
if not begin_headers and 'BEGIN PGP %s' % kind in line:
begin_headers = True

View File

@@ -307,7 +307,7 @@ class XEP_0030(BasePlugin):
return self.api['has_identity'](jid, node, ifrom, data)
async def get_info_from_domain(self, domain=None, timeout=None,
cached=True, callback=None):
cached=True, callback=None, **iqkwargs):
"""Fetch disco#info of specified domain and one disco#items level below
"""
@@ -315,13 +315,13 @@ class XEP_0030(BasePlugin):
domain = self.xmpp.boundjid.domain
if not cached or domain not in self.domain_infos:
infos = [self.get_info(
domain, timeout=timeout)]
infos = [asyncio.create_task(self.get_info(
domain, timeout=timeout, **iqkwargs))]
iq_items = await self.get_items(
domain, timeout=timeout)
domain, timeout=timeout, **iqkwargs)
items = iq_items['disco_items']['items']
infos += [
self.get_info(item[0], timeout=timeout)
asyncio.create_task(self.get_info(item[0], timeout=timeout, **iqkwargs))
for item in items]
info_futures, _ = await asyncio.wait(
infos,
@@ -457,9 +457,12 @@ class XEP_0030(BasePlugin):
the XEP-0059 plugin, if the plugin is loaded.
Otherwise the parameter is ignored.
"""
if ifrom is None and self.xmpp.is_component:
ifrom = self.xmpp.boundjid.bare
if local or local is None and jid is None:
items = await self.api['get_items'](jid, node, ifrom, kwargs)
return self._wrap(kwargs.get('ifrom', None), jid, items)
return self._wrap(ifrom, jid, items)
iq = self.xmpp.Iq()
# Check dfrom parameter for backwards compatibility

View File

@@ -323,7 +323,6 @@ class XEP_0045(BasePlugin):
def add_message(msg: Message):
delay = msg.get_plugin('delay', check=True)
print(delay)
if delay is not None and delay['from'] == room:
history_buffer.append(msg)

View File

@@ -4,6 +4,7 @@
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
import asyncio
import functools
import logging
import time
@@ -619,10 +620,16 @@ class XEP_0050(BasePlugin):
self.terminate_command(session)
def _iscoroutine_or_partial_coroutine(handler):
return asyncio.iscoroutinefunction(handler) \
or isinstance(handler, functools.partial) \
and asyncio.iscoroutinefunction(handler.func)
async def _await_if_needed(handler, *args):
if handler is None:
raise XMPPError("bad-request", text="The command is completed")
if asyncio.iscoroutinefunction(handler):
if _iscoroutine_or_partial_coroutine(handler):
log.debug(f"%s is async", handler)
return await handler(*args)
else:

View File

@@ -134,8 +134,10 @@ class XEP_0054(BasePlugin):
return
elif iq['type'] == 'get' and self.xmpp.is_component:
vcard = await self.api['get_vcard'](iq['to'].bare, ifrom=iq['from'])
if isinstance(vcard, Iq):
vcard.send()
if vcard is None:
raise XMPPError("item-not-found")
elif isinstance(vcard, Iq):
await vcard.send()
else:
iq = iq.reply()
iq.append(vcard)

View File

@@ -7,7 +7,8 @@ import logging
import hashlib
import base64
from asyncio import Future
from asyncio import Future, Lock
from collections import defaultdict
from typing import Optional
from slixmpp import __version__
@@ -94,6 +95,9 @@ class XEP_0115(BasePlugin):
disco.assign_verstring = self.assign_verstring
disco.get_verstring = self.get_verstring
# prevent concurrent fetches for the same hash
self._locks = defaultdict(Lock)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace)
self.xmpp.del_filter('out', self._filter_add_caps)
@@ -137,7 +141,7 @@ class XEP_0115(BasePlugin):
self.xmpp.event('entity_caps', p)
async def _process_caps(self, pres):
async def _process_caps(self, pres: Presence):
if not pres['caps']['hash']:
log.debug("Received unsupported legacy caps: %s, %s, %s",
pres['caps']['node'],
@@ -147,7 +151,11 @@ class XEP_0115(BasePlugin):
return
ver = pres['caps']['ver']
async with self._locks[ver]:
await self._process_caps_wrapped(pres, ver)
self._locks.pop(ver, None)
async def _process_caps_wrapped(self, pres: Presence, ver: str):
existing_verstring = await self.get_verstring(pres['from'].full)
if str(existing_verstring) == str(ver):
return
@@ -162,7 +170,7 @@ class XEP_0115(BasePlugin):
if pres['caps']['hash'] not in self.hashes:
try:
log.debug("Unknown caps hash: %s", pres['caps']['hash'])
self.xmpp['xep_0030'].get_info(jid=pres['from'], ifrom=ifrom)
await self.xmpp['xep_0030'].get_info(jid=pres['from'], ifrom=ifrom)
return
except XMPPError:
return

View File

@@ -137,7 +137,14 @@ class XEP_0199(BasePlugin):
async def _keepalive(self, event=None):
log.debug("Keepalive ping...")
try:
rtt = await self.ping(self.xmpp.boundjid.host, timeout=self.timeout)
ifrom = None
if self.xmpp.is_component:
ifrom = self.xmpp.boundjid
rtt = await self.ping(
self.xmpp.boundjid.host,
timeout=self.timeout,
ifrom=ifrom
)
except IqTimeout:
log.debug("Did not receive ping back in time. " + \
"Requesting Reconnect.")

View File

@@ -15,6 +15,32 @@ log = logging.getLogger(__name__)
class XEP_0221(BasePlugin):
"""
XEP-0221: Data Forms Media Element
In certain implementations of Data Forms (XEP-0004), it can be
helpful to include media data such as small images. One example is
CAPTCHA Forms (XEP-0158). This plugin implements a method for
including media data in a data form.
Typical use pattern:
.. code-block:: python
self.register_plugin('xep_0221')
self['xep_0050'].add_command(node="showimage",
name="Show my image",
handler=self.form_handler)
def form_handler(self,iq,session):
image_url="https://xmpp.org/images/logos/xmpp-logo.svg"
form=self['xep_0004'].make_form('result','My Image')
form.addField(var='myimage', ftype='text-single', label='My Image', value=image_url)
form.field['myimage']['media'].add_uri(value=image_url, itype="image/svg")
session['payload']=form
return session
"""
name = 'xep_0221'
description = 'XEP-0221: Data Forms Media Element'

View File

@@ -0,0 +1,6 @@
from slixmpp.plugins.base import register_plugin
from . import stanza
from .jingle_file_transfer import XEP_0234
register_plugin(XEP_0234)

View File

@@ -0,0 +1,21 @@
import logging
from slixmpp.plugins import BasePlugin
from . import stanza
log = logging.getLogger(__name__)
class XEP_0234(BasePlugin):
"""
XEP-0234: Jingle File Transfer
Minimum needed for xep 0385 (Stateless inline media sharing)
"""
name = "xep_0234"
description = "XEP-0234: Jingle File Transfer"
dependencies = {"xep_0082", "xep_0300"}
stanza = stanza

View File

@@ -0,0 +1,38 @@
from datetime import datetime
from slixmpp.plugins.xep_0082 import format_datetime, parse
from slixmpp.xmlstream import ElementBase
NS = "urn:xmpp:jingle:apps:file-transfer:5"
class File(ElementBase):
name = "file"
namespace = NS
plugin_attrib = "file"
interfaces = sub_interfaces = {"media-type", "name", "date", "size", "hash", "desc"}
def set_size(self, size: int):
self._set_sub_text("size", str(size))
def get_size(self):
return _int_or_none(self._get_sub_text("size"))
def get_date(self):
try:
return parse(self._get_sub_text("date"))
except ValueError:
return
def set_date(self, stamp: datetime):
try:
self._set_sub_text("date", format_datetime(stamp))
except ValueError:
pass
def _int_or_none(v):
try:
return int(v)
except ValueError:
return None

View File

@@ -0,0 +1,5 @@
from slixmpp.plugins.base import register_plugin
from .thumbnail import XEP_0264
register_plugin(XEP_0264)

View File

@@ -0,0 +1,36 @@
from typing import Optional
from slixmpp import register_stanza_plugin
from slixmpp.plugins.xep_0234.stanza import File
from slixmpp.xmlstream import ElementBase
NS = "urn:xmpp:thumbs:1"
class Thumbnail(ElementBase):
name = plugin_attrib = "thumbnail"
namespace = NS
interfaces = {"uri", "media-type", "width", "height"}
def get_width(self) -> int:
return _int_or_none(self._get_attr("width"))
def get_height(self) -> int:
return _int_or_none(self._get_attr("height"))
def set_width(self, v: int) -> None:
self._set_attr("width", str(v))
def set_height(self, v: int) -> None:
self._set_attr("height", str(v))
def _int_or_none(v) -> Optional[int]:
try:
return int(v)
except ValueError:
return None
def register_plugin():
register_stanza_plugin(File, Thumbnail)

View File

@@ -0,0 +1,24 @@
import logging
from slixmpp.plugins import BasePlugin
from . import stanza
log = logging.getLogger(__name__)
class XEP_0264(BasePlugin):
"""
XEP-0264: Jingle Content Thumbnails
Can also be used with 0385 (Stateless inline media sharing)
"""
name = "xep_0264"
description = "XEP-0264: Jingle Content Thumbnails"
dependencies = {"xep_0234"}
stanza = stanza
def plugin_init(self):
stanza.register_plugin()

View File

@@ -0,0 +1,5 @@
from slixmpp.plugins.base import register_plugin
from . import stanza, vcard4
register_plugin(vcard4.XEP_0292)

View File

@@ -0,0 +1,167 @@
import datetime
from typing import Optional
from slixmpp import ElementBase, Iq, register_stanza_plugin
NS = "urn:ietf:params:xml:ns:vcard-4.0"
class _VCardElementBase(ElementBase):
namespace = NS
class VCard4(_VCardElementBase):
name = plugin_attrib = "vcard"
interfaces = {"full_name", "given", "surname", "birthday"}
def set_full_name(self, full_name: str):
self["fn"]["text"] = full_name
def get_full_name(self):
return self["fn"]["text"]
def set_given(self, given: str):
self["n"]["given"] = given
def get_given(self):
return self["n"]["given"]
def set_surname(self, surname: str):
self["n"]["surname"] = surname
def get_surname(self):
return self["n"]["surname"]
def set_birthday(self, birthday: datetime.date):
self["bday"]["date"] = birthday
def get_birthday(self):
return self["bday"]["date"]
def add_tel(self, number: str, name: Optional[str] = None):
tel = Tel()
if name:
tel["parameters"]["type_"]["text"] = name
tel["uri"] = f"tel:{number}"
self.append(tel)
def add_address(
self, country: Optional[str] = None, locality: Optional[str] = None
):
adr = Adr()
if locality:
adr["locality"] = locality
if country:
adr["country"] = country
self.append(adr)
def add_nickname(self, nick: str):
el = Nickname()
el["text"] = nick
self.append(el)
def add_note(self, note: str):
el = Note()
el["text"] = note
self.append(el)
def add_impp(self, impp: str):
el = Impp()
el["uri"] = impp
self.append(el)
def add_url(self, url: str):
el = Url()
el["uri"] = url
self.append(el)
def add_email(self, email: str):
el = Email()
el["text"] = email
self.append(el)
class _VCardTextElementBase(_VCardElementBase):
interfaces = {"text"}
sub_interfaces = {"text"}
class Fn(_VCardTextElementBase):
name = plugin_attrib = "fn"
class Nickname(_VCardTextElementBase):
name = plugin_attrib = "nickname"
class Note(_VCardTextElementBase):
name = plugin_attrib = "note"
class _VCardUriElementBase(_VCardElementBase):
interfaces = {"uri"}
sub_interfaces = {"uri"}
class Url(_VCardUriElementBase):
name = plugin_attrib = "url"
class Impp(_VCardUriElementBase):
name = plugin_attrib = "impp"
class Email(_VCardTextElementBase):
name = plugin_attrib = "email"
class N(_VCardElementBase):
name = "n"
plugin_attrib = "n"
interfaces = sub_interfaces = {"given", "surname", "additional"}
class BDay(_VCardElementBase):
name = plugin_attrib = "bday"
interfaces = {"date"}
def set_date(self, date: datetime.date):
d = Date()
d.xml.text = date.strftime("%Y-%m-%d")
self.append(d)
def get_date(self):
for elem in self.xml:
try:
return datetime.date.fromisoformat(elem.text)
except ValueError:
return None
class Date(_VCardElementBase):
name = "date"
class Tel(_VCardUriElementBase):
name = plugin_attrib = "tel"
class Parameters(_VCardElementBase):
name = plugin_attrib = "parameters"
class Type(_VCardTextElementBase):
name = "type"
plugin_attrib = "type_"
class Adr(_VCardElementBase):
name = plugin_attrib = "adr"
interfaces = sub_interfaces = {"locality", "country"}
register_stanza_plugin(Parameters, Type)
register_stanza_plugin(Tel, Parameters)
for p in N, Fn, Nickname, Note, Url, Impp, Email, BDay, Tel, Adr:
register_stanza_plugin(VCard4, p, iterable=True)
register_stanza_plugin(Iq, VCard4)

View File

@@ -0,0 +1,111 @@
import logging
from datetime import date
from typing import Optional
from slixmpp import (
JID,
ComponentXMPP,
register_stanza_plugin,
)
from slixmpp.plugins.base import BasePlugin
from . import stanza
class XEP_0292(BasePlugin):
"""
vCard4 over XMPP
Does not implement the IQ semantics that neither movim does gajim implement,
cf https://xmpp.org/extensions/xep-0292.html#self-iq-retrieval and
https://xmpp.org/extensions/xep-0292.html#self-iq-publication
Does not implement the "empty pubsub event item" as a notification mechanism,
that neither gajim nor movim implement
https://xmpp.org/extensions/xep-0292.html#sect-idm45744791178720
Relies on classic pubsub semantics instead.
"""
xmpp: ComponentXMPP
name = "xep_0292"
description = "vCard4 Over XMPP"
dependencies = {"xep_0163", "xep_0060", "xep_0030"}
stanza = stanza
def plugin_init(self):
pubsub_stanza = self.xmpp["xep_0060"].stanza
register_stanza_plugin(pubsub_stanza.Item, stanza.VCard4)
register_stanza_plugin(pubsub_stanza.EventItem, stanza.VCard4)
self.xmpp['xep_0060'].map_node_event(stanza.NS, 'vcard4')
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=stanza.NS)
self.xmpp['xep_0163'].remove_interest(stanza.NS)
def session_bind(self, jid):
self.xmpp['xep_0163'].register_pep('vcard4', stanza.VCard4)
def publish_vcard(
self,
full_name: Optional[str] = None,
given: Optional[str] = None,
surname: Optional[str] = None,
birthday: Optional[date] = None,
nickname: Optional[str] = None,
phone: Optional[str] = None,
note: Optional[str] = None,
url: Optional[str] = None,
email: Optional[str] = None,
country: Optional[str] = None,
locality: Optional[str] = None,
impp: Optional[str] = None,
**pubsubkwargs,
):
"""
Publish a vcard using PEP
"""
vcard = stanza.VCard4()
if impp:
vcard.add_impp(impp)
if nickname:
vcard.add_nickname(nickname)
if full_name:
vcard["full_name"] = full_name
if given:
vcard["given"] = given
if surname:
vcard["surname"] = surname
if birthday:
vcard["birthday"] = birthday
if note:
vcard.add_note(note)
if url:
vcard.add_url(url)
if email:
vcard.add_email(email)
if phone:
vcard.add_tel(phone)
if country and locality:
vcard.add_address(country, locality)
elif country:
vcard.add_address(country, locality)
return self.xmpp["xep_0163"].publish(vcard, id="current", **pubsubkwargs)
def retrieve_vcard(self, jid: JID, **pubsubkwargs):
"""
Retrieve a vcard using PEP
"""
return self.xmpp["xep_0060"].get_item(
jid, stanza.VCard4.namespace, "current", **pubsubkwargs
)
log = logging.getLogger(__name__)

View File

@@ -52,9 +52,10 @@ class MAM(ElementBase):
#: fetch, not relevant for the stanza itself.
interfaces = {
'queryid', 'start', 'end', 'with', 'results',
'before_id', 'after_id', 'ids',
'before_id', 'after_id', 'ids', 'flip_page',
}
sub_interfaces = {'start', 'end', 'with', 'before_id', 'after_id', 'ids'}
sub_interfaces = {'start', 'end', 'with', 'before_id', 'after_id', 'ids',
'flip_page'}
def setup(self, xml=None):
ElementBase.setup(self, xml)
@@ -81,7 +82,7 @@ class MAM(ElementBase):
def get_start(self) -> Optional[datetime]:
fields = self.get_fields()
field = fields.get('start')
if field:
if field and field["value"]:
return xep_0082.parse(field['value'])
return None
@@ -94,7 +95,7 @@ class MAM(ElementBase):
def get_end(self) -> Optional[datetime]:
fields = self.get_fields()
field = fields.get('end')
if field:
if field and field["value"]:
return xep_0082.parse(field['value'])
return None
@@ -168,6 +169,8 @@ class MAM(ElementBase):
def del_results(self):
self._results = []
def get_flip_page(self):
return self.xml.find(f'{{{self.namespace}}}flip-page') is not None
class Fin(ElementBase):
"""A MAM fin element (end of query).
@@ -187,7 +190,7 @@ class Fin(ElementBase):
name = 'fin'
namespace = 'urn:xmpp:mam:2'
plugin_attrib = 'mam_fin'
interfaces = {'results'}
interfaces = {'results', 'stable', 'complete'}
def setup(self, xml=None):
ElementBase.setup(self, xml)

View File

@@ -0,0 +1,11 @@
# Slixmpp: The Slick XMPP Library
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from slixmpp.plugins import register_plugin
from slixmpp.plugins.xep_0317 import stanza
from slixmpp.plugins.xep_0317.hats import XEP_0317
from slixmpp.plugins.xep_0317.stanza import Hat, Hats
register_plugin(XEP_0317)
__all__ = ['stanza', 'XEP_317']

View File

@@ -0,0 +1,16 @@
from slixmpp.plugins import BasePlugin
from . import stanza
class XEP_0317(BasePlugin):
"""
XEP-0317: Hats
"""
name = 'xep_0317'
description = 'XEP-0317: Hats'
dependencies = {'xep_0030', 'xep_0045', 'xep_0050'}
stanza = stanza
namespace = stanza.NS
def plugin_init(self):
stanza.register_plugin()

View File

@@ -0,0 +1,58 @@
from slixmpp import Presence
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
from typing import List, Tuple
NS = 'urn:xmpp:hats:0'
class Hats(ElementBase):
"""
Hats element, container for multiple hats:
.. code-block::xml
<hats xmlns='urn:xmpp:hats:0'>
<hat title='Host' uri='http://schemas.example.com/hats#host' xml:lang='en-us'>
<badge xmlns="urn:example:badges" fgcolor="#000000" bgcolor="#58C5BA"/>
</hat>
<hat title='Presenter' uri='http://schemas.example.com/hats#presenter' xml:lang='en-us'>
<badge xmlns="urn:example:badges" fgcolor="#000000" bgcolor="#EC0524"/>
</hat>
</hats>
"""
name = 'hats'
namespace = NS
plugin_attrib = 'hats'
def add_hats(self, data: List[Tuple[str, str]]) -> None:
for uri, title in data:
hat = Hat()
hat["uri"] = uri
hat["title"] = title
self.append(hat)
class Hat(ElementBase):
"""
Hat element, has a title and url, may contain arbitrary sub-elements.
.. code-block::xml
<hat title='Host' uri='http://schemas.example.com/hats#host' xml:lang='en-us'>
<badge xmlns="urn:example:badges" fgcolor="#000000" bgcolor="#58C5BA"/>
</hat>
"""
name = 'hat'
plugin_attrib = 'hat'
namespace = NS
interfaces = {'title', 'uri'}
plugin_multi_attrib = "hats"
def register_plugin() -> None:
register_stanza_plugin(Hats, Hat, iterable=True)
register_stanza_plugin(Presence, Hats)

View File

@@ -1,7 +1,7 @@
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0356 import stanza
from slixmpp.plugins.xep_0356.stanza import Perm, Privilege
from slixmpp.plugins.xep_0356.privilege import XEP_0356
from . import stanza
from .privilege import XEP_0356
from .stanza import Perm, Privilege
register_plugin(XEP_0356)

View File

@@ -0,0 +1,36 @@
import dataclasses
from collections import defaultdict
from enum import Enum
class RosterAccess(str, Enum):
NONE = "none"
GET = "get"
SET = "set"
BOTH = "both"
class MessagePermission(str, Enum):
NONE = "none"
OUTGOING = "outgoing"
class IqPermission(str, Enum):
NONE = "none"
GET = "get"
SET = "set"
BOTH = "both"
class PresencePermission(str, Enum):
NONE = "none"
MANAGED_ENTITY = "managed_entity"
ROSTER = "roster"
@dataclasses.dataclass
class Permissions:
roster = RosterAccess.NONE
message = MessagePermission.NONE
iq = defaultdict(lambda: IqPermission.NONE)
presence = PresencePermission.NONE

View File

@@ -1,14 +1,16 @@
import logging
import typing
import uuid
from collections import defaultdict
from slixmpp import Message, JID, Iq
from slixmpp import JID, Iq, Message
from slixmpp.plugins.base import BasePlugin
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import StanzaBase
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins.xep_0356 import stanza, Privilege, Perm
from slixmpp.xmlstream.matcher import StanzaPath
from . import stanza
from .permissions import IqPermission, MessagePermission, Permissions, RosterAccess
log = logging.getLogger(__name__)
@@ -29,7 +31,7 @@ class XEP_0356(BasePlugin):
dependencies = {"xep_0297"}
stanza = stanza
granted_privileges = {"roster": "none", "message": "none", "presence": "none"}
granted_privileges = defaultdict(Permissions)
def plugin_init(self):
if not self.xmpp.is_component:
@@ -49,32 +51,42 @@ class XEP_0356(BasePlugin):
def plugin_end(self):
self.xmpp.remove_handler("Privileges")
def _handle_privilege(self, msg: Message):
def _handle_privilege(self, msg: StanzaBase):
"""
Called when the XMPP server advertise the component's privileges.
Stores the privileges in this instance's granted_privileges attribute (a dict)
and raises the privileges_advertised event
"""
permissions = self.granted_privileges[msg.get_from()]
for perm in msg["privilege"]["perms"]:
self.granted_privileges[perm["access"]] = perm["type"]
access = perm["access"]
if access == "iq":
for ns in perm["namespaces"]:
permissions.iq[ns["ns"]] = ns["type"]
elif access in _VALID_ACCESSES:
setattr(permissions, access, perm["type"])
else:
log.warning("Received an invalid privileged access: %s", access)
log.debug(f"Privileges: {self.granted_privileges}")
self.xmpp.event("privileges_advertised")
def send_privileged_message(self, msg: Message):
if self.granted_privileges["message"] == "outgoing":
self._make_privileged_message(msg).send()
else:
log.error(
if (
self.granted_privileges[msg.get_from().domain].message
!= MessagePermission.OUTGOING
):
raise PermissionError(
"The server hasn't authorized us to send messages on behalf of other users"
)
else:
self._make_privileged_message(msg).send()
def _make_privileged_message(self, msg: Message):
stanza = self.xmpp.make_message(
mto=self.xmpp.server_host, mfrom=self.xmpp.boundjid.bare
)
stanza["privilege"]["forwarded"].append(msg)
return stanza
server = msg.get_from().domain
wrapped = self.xmpp.make_message(mto=server, mfrom=self.xmpp.boundjid.bare)
wrapped["privilege"]["forwarded"].append(msg)
return wrapped
def _make_get_roster(self, jid: typing.Union[JID, str], **iq_kwargs):
return self.xmpp.make_iq_get(
@@ -106,9 +118,15 @@ class XEP_0356(BasePlugin):
:param jid: user we want to fetch the roster from
"""
if self.granted_privileges["roster"] not in ("get", "both"):
log.error("The server did not grant us privileges to get rosters")
raise ValueError
if isinstance(jid, str):
jid = JID(jid)
if self.granted_privileges[jid.domain].roster not in (
RosterAccess.GET,
RosterAccess.BOTH,
):
raise PermissionError(
"The server did not grant us privileges to get rosters"
)
else:
return await self._make_get_roster(jid).send(**send_kwargs)
@@ -137,8 +155,56 @@ class XEP_0356(BasePlugin):
},
}
"""
if self.granted_privileges["roster"] not in ("set", "both"):
log.error("The server did not grant us privileges to set rosters")
raise ValueError
if isinstance(jid, str):
jid = JID(jid)
if self.granted_privileges[jid.domain].roster not in (
RosterAccess.GET,
RosterAccess.BOTH,
):
raise PermissionError(
"The server did not grant us privileges to set rosters"
)
else:
return await self._make_set_roster(jid, roster_items).send(**send_kwargs)
async def send_privileged_iq(
self, encapsulated_iq: Iq, iq_id: typing.Optional[str] = None
):
"""
Send an IQ on behalf of a user
Caution: the IQ *must* have the jabber:client namespace
"""
iq_id = iq_id or str(uuid.uuid4())
encapsulated_iq["id"] = iq_id
server = encapsulated_iq.get_to().domain
perms = self.granted_privileges.get(server)
if not perms:
raise PermissionError(f"{server} has not granted us any privilege")
itype = encapsulated_iq["type"]
for ns in encapsulated_iq.plugins.values():
type_ = perms.iq[ns.namespace]
if type_ == IqPermission.NONE:
raise PermissionError(
f"{server} has not granted any IQ privilege for namespace {ns.namespace}"
)
elif type_ == IqPermission.BOTH:
pass
elif type_ != itype:
raise PermissionError(
f"{server} has not granted IQ {itype} privilege for namespace {ns.namespace}"
)
iq = self.xmpp.make_iq(
itype=itype,
ifrom=self.xmpp.boundjid.bare,
ito=encapsulated_iq.get_from(),
id=iq_id,
)
iq["privileged_iq"].append(encapsulated_iq)
resp = await iq.send()
return resp["privilege"]["forwarded"]["iq"]
# does not include iq access that is handled differently
_VALID_ACCESSES = {"message", "roster", "presence"}

View File

@@ -1,13 +1,12 @@
from slixmpp.stanza import Message
from slixmpp.xmlstream import (
ElementBase,
register_stanza_plugin,
)
from slixmpp.plugins.xep_0297 import Forwarded
from slixmpp.stanza import Iq, Message
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
NS = "urn:xmpp:privilege:2"
class Privilege(ElementBase):
namespace = "urn:xmpp:privilege:2"
namespace = NS
name = "privilege"
plugin_attrib = "privilege"
@@ -25,26 +24,40 @@ class Privilege(ElementBase):
def presence(self):
return self.permission("presence")
def iq(self):
return self.permission("iq")
def add_perm(self, access, type):
def add_perm(self, access, type_):
# This should only be needed for servers, so maybe out of scope for slixmpp
perm = Perm()
perm["type"] = type
perm["type"] = type_
perm["access"] = access
self.append(perm)
class Perm(ElementBase):
namespace = "urn:xmpp:privilege:2"
namespace = NS
name = "perm"
plugin_attrib = "perm"
plugin_multi_attrib = "perms"
interfaces = {"type", "access"}
class NameSpace(ElementBase):
namespace = NS
name = "namespace"
plugin_attrib = "namespace"
plugin_multi_attrib = "namespaces"
interfaces = {"ns", "type"}
class PrivilegedIq(ElementBase):
namespace = NS
name = "privileged_iq"
plugin_attrib = "privileged_iq"
def register():
register_stanza_plugin(Message, Privilege)
register_stanza_plugin(Iq, Privilege)
register_stanza_plugin(Privilege, Forwarded)
register_stanza_plugin(Privilege, Perm, iterable=True)
register_stanza_plugin(Perm, NameSpace, iterable=True)
register_stanza_plugin(Iq, PrivilegedIq)

View File

@@ -0,0 +1,6 @@
from slixmpp.plugins.base import register_plugin
from . import stanza
from .references import XEP_0372
register_plugin(XEP_0372)

View File

@@ -0,0 +1,23 @@
import logging
from slixmpp import Message, register_stanza_plugin
from slixmpp.plugins import BasePlugin
from . import stanza
log = logging.getLogger(__name__)
class XEP_0372(BasePlugin):
"""
XEP-0372: References
Minimum needed for xep 0385 (Stateless inline media sharing)
"""
name = "xep_0372"
description = "XEP-0372: References"
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Message, stanza.Reference)

View File

@@ -0,0 +1,9 @@
from slixmpp.xmlstream import ElementBase
NAMESPACE = "urn:xmpp:reference:0"
class Reference(ElementBase):
name = plugin_attrib = "reference"
namespace = NAMESPACE
interfaces = {"type", "uri", "id", "begin", "end"}

View File

@@ -26,6 +26,9 @@ class XEP_0377(BasePlugin):
dependencies = {'xep_0030', 'xep_0191'}
stanza = stanza
SPAM = 'urn:xmpp:reporting:spam'
ABUSE = 'urn:xmpp:reporting:abuse'
def plugin_init(self):
register_stanza_plugin(Block, stanza.Report)
register_stanza_plugin(stanza.Report, stanza.Text)

View File

@@ -13,58 +13,23 @@ class Report(ElementBase):
Example sub stanza:
::
<report xmlns="urn:xmpp:reporting:0">
<report xmlns="urn:xmpp:reporting:1" reason="urn:xmpp:reporting:abuse">
<text xml:lang="en">
Never came trouble to my house like this.
</text>
<spam/>
</report>
Stanza Interface:
::
The reason attribute is mandatory.
abuse -- Flag the report as abuse
spam -- Flag the report as spam
text -- Add a reason to the report
Only one <spam/> or <abuse/> element can be present at once.
"""
name = "report"
namespace = "urn:xmpp:reporting:0"
namespace = "urn:xmpp:reporting:1"
plugin_attrib = "report"
interfaces = ("spam", "abuse", "text")
interfaces = ("text", "reason")
sub_interfaces = {'text'}
def _purge_spam(self):
spam = self.xml.findall('{%s}spam' % self.namespace)
for element in spam:
self.xml.remove(element)
def _purge_abuse(self):
abuse = self.xml.findall('{%s}abuse' % self.namespace)
for element in abuse:
self.xml.remove(element)
def get_spam(self):
return self.xml.find('{%s}spam' % self.namespace) is not None
def set_spam(self, value):
self._purge_spam()
if bool(value):
self._purge_abuse()
self.xml.append(ET.Element('{%s}spam' % self.namespace))
def get_abuse(self):
return self.xml.find('{%s}abuse' % self.namespace) is not None
def set_abuse(self, value):
self._purge_abuse()
if bool(value):
self._purge_spam()
self.xml.append(ET.Element('{%s}abuse' % self.namespace))
class Text(ElementBase):
name = "text"
plugin_attrib = "text"
namespace = "urn:xmpp:reporting:0"
namespace = "urn:xmpp:reporting:1"

View File

@@ -0,0 +1,11 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
# This file is part of Slixmpp.
# See the file LICENSE for copying permission
from slixmpp.plugins.base import register_plugin
from . import stanza
from .sims import XEP_0385
register_plugin(XEP_0385)

View File

@@ -0,0 +1,66 @@
import logging
from datetime import datetime
from pathlib import Path
from typing import Iterable, Optional
from slixmpp.plugins import BasePlugin
from slixmpp.stanza import Message
from slixmpp.xmlstream import register_stanza_plugin
from . import stanza
log = logging.getLogger(__name__)
class XEP_0385(BasePlugin):
"""
XEP-0385: Stateless Inline Media Sharing (SIMS)
Only support outgoing SIMS, incoming is not handled at all.
"""
name = "xep_0385"
description = "XEP-0385: Stateless Inline Media Sharing (SIMS)"
dependencies = {"xep_0234", "xep_0300", "xep_0372"}
stanza = stanza
def plugin_init(self):
register_stanza_plugin(self.xmpp["xep_0372"].stanza.Reference, stanza.Sims)
register_stanza_plugin(Message, stanza.Sims)
register_stanza_plugin(stanza.Sims, stanza.Sources)
register_stanza_plugin(stanza.Sims, self.xmpp["xep_0234"].stanza.File)
register_stanza_plugin(stanza.Sources, self.xmpp["xep_0372"].stanza.Reference)
def get_sims(
self,
path: Path,
uris: Iterable[str],
media_type: Optional[str],
desc: Optional[str],
):
sims = stanza.Sims()
for uri in uris:
ref = self.xmpp["xep_0372"].stanza.Reference()
ref["uri"] = uri
ref["type"] = "data"
sims["sources"].append(ref)
if media_type:
sims["file"]["media-type"] = media_type
if desc:
sims["file"]["desc"] = desc
sims["file"]["name"] = path.name
stat = path.stat()
sims["file"]["size"] = stat.st_size
sims["file"]["date"] = datetime.fromtimestamp(stat.st_mtime)
h = self.xmpp.plugin["xep_0300"].compute_hash(path)
h["value"] = h["value"].decode()
sims["file"].append(h)
ref = self.xmpp["xep_0372"].stanza.Reference()
ref.append(sims)
ref["type"] = "data"
return ref

View File

@@ -0,0 +1,14 @@
from slixmpp.xmlstream import ElementBase
NAMESPACE = "urn:xmpp:sims:1"
class Sims(ElementBase):
name = "media-sharing"
plugin_attrib = "sims"
namespace = NAMESPACE
class Sources(ElementBase):
name = plugin_attrib = "sources"
namespace = NAMESPACE

View File

@@ -0,0 +1,6 @@
from slixmpp.plugins.base import register_plugin
from . import stanza
from .bookmarks import XEP_0402
register_plugin(XEP_0402)

View File

@@ -0,0 +1,18 @@
from slixmpp.plugins import BasePlugin
from . import stanza
class XEP_0402(BasePlugin):
"""
XEP-0402: PEP Native bookmarks
"""
name = "xep_0402"
description = "XEP-0402: PEP Native bookmarks"
dependencies = {"xep_0402"}
stanza = stanza
def plugin_init(self):
stanza.register_plugin()

View File

@@ -0,0 +1,33 @@
from slixmpp import register_stanza_plugin
from slixmpp.plugins.xep_0060.stanza import Item
from slixmpp.xmlstream import ElementBase
NS = "urn:xmpp:bookmarks:1"
class Conference(ElementBase):
namespace = NS
name = "conference"
plugin_attrib = "conference"
interfaces = {"name", "autojoin", "nick", "password"}
sub_interfaces = {"nick", "password"}
def set_autojoin(self, v: bool):
self._set_attr("autojoin", "true" if v else "false")
def get_autojoin(self):
v = self._get_attr("autojoin", "")
if not v:
return False
return v == "1" or v.lower() == "true"
class Extensions(ElementBase):
namespace = NS
name = "extensions"
plugin_attrib = "extensions"
def register_plugin():
register_stanza_plugin(Conference, Extensions)
register_stanza_plugin(Item, Conference)

View File

@@ -1,8 +1,13 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2020 Mathieu Pasquet <mathieui@mathieui.net>
# This file is part of Slixmpp.
# See the file LICENSE for copying permissio
from abc import ABC
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
from slixmpp.stanza import Message
from slixmpp.xmlstream import (
ElementBase,
@@ -10,14 +15,83 @@ from slixmpp.xmlstream import (
)
NS = 'urn:xmpp:fallback:0'
NS = "urn:xmpp:fallback:0"
class Fallback(ElementBase):
namespace = NS
name = 'fallback'
plugin_attrib = 'fallback'
name = "fallback"
plugin_attrib = "fallback"
plugin_multi_attrib = "fallbacks"
interfaces = {"for"}
def _find_fallback(self, fallback_for: str) -> "Fallback":
if self["for"] == fallback_for:
return self
for fallback in self.parent()["fallbacks"]:
if fallback["for"] == fallback_for:
return fallback
raise AttributeError("No fallback for this namespace", fallback_for)
def get_stripped_body(
self, fallback_for: str, element: Literal["body", "subject"] = "body"
) -> str:
"""
Get the body of a message, with the fallback part stripped
:param fallback_for: namespace of the fallback to strip
:param element: set this to "subject" get the stripped subject instead
of body
:return: body (or subject) content minus the fallback part
"""
fallback = self._find_fallback(fallback_for)
start = fallback[element]["start"]
end = fallback[element]["end"]
body = self.parent()[element]
if start == end == 0:
return ""
if start <= end < len(body):
return body[:start] + body[end:]
else:
return body
class FallbackMixin(ABC):
namespace = NS
name = NotImplemented
plugin_attrib = NotImplemented
interfaces = {"start", "end"}
def set_start(self, v: int):
self._set_attr("start", str(v))
def get_start(self):
return _int_or_zero(self._get_attr("start"))
def set_end(self, v: int):
self._set_attr("end", str(v))
def get_end(self):
return _int_or_zero(self._get_attr("end"))
class FallbackBody(FallbackMixin, ElementBase):
name = plugin_attrib = "body"
class FallbackSubject(FallbackMixin, ElementBase):
name = plugin_attrib = "subject"
def _int_or_zero(v: str):
try:
return int(v)
except ValueError:
return 0
def register_plugins():
register_stanza_plugin(Message, Fallback)
register_stanza_plugin(Message, Fallback, iterable=True)
register_stanza_plugin(Fallback, FallbackBody)
register_stanza_plugin(Fallback, FallbackSubject)

View File

@@ -0,0 +1,6 @@
from slixmpp.plugins.base import register_plugin
from . import stanza
from .file_metadata import XEP_0446
register_plugin(XEP_0446)

View File

@@ -0,0 +1,20 @@
import logging
from slixmpp.plugins import BasePlugin
from . import stanza
log = logging.getLogger(__name__)
class XEP_0446(BasePlugin):
"""
XEP-0446: File metadata element
Minimum needed for xep 0447 (Stateless file sharing)
"""
name = "xep_0446"
description = "XEP-0446: File metadata element"
stanza = stanza

View File

@@ -0,0 +1,38 @@
from datetime import datetime
from slixmpp.plugins.xep_0082 import format_datetime, parse
from slixmpp.xmlstream import ElementBase
NS = "urn:xmpp:file:metadata:0"
class File(ElementBase):
name = "file"
namespace = NS
plugin_attrib = "file"
interfaces = sub_interfaces = {"media-type", "name", "date", "size", "hash", "desc"}
def set_size(self, size: int):
self._set_sub_text("size", str(size))
def get_size(self):
return _int_or_none(self._get_sub_text("size"))
def get_date(self):
try:
return parse(self._get_sub_text("date"))
except ValueError:
return
def set_date(self, stamp: datetime):
try:
self._set_sub_text("date", format_datetime(stamp))
except ValueError:
pass
def _int_or_none(v):
try:
return int(v)
except ValueError:
return None

View File

@@ -0,0 +1,11 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
# This file is part of Slixmpp.
# See the file LICENSE for copying permission
from slixmpp.plugins.base import register_plugin
from . import stanza
from .sfs import XEP_0447
register_plugin(XEP_0447)

View File

@@ -0,0 +1,64 @@
import logging
from datetime import datetime
from pathlib import Path
from typing import Iterable, Optional
from slixmpp.plugins import BasePlugin
from slixmpp.stanza import Message
from slixmpp.xmlstream import register_stanza_plugin
from . import stanza
log = logging.getLogger(__name__)
class XEP_0447(BasePlugin):
"""
XEP-0447: Stateless File Sharing
Only support outgoing SFS, incoming is not handled at all.
"""
name = "xep_0447"
description = "XEP-0447: Stateless File Sharing"
dependencies = {"xep_0300", "xep_0446"}
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Message, stanza.StatelessFileSharing)
register_stanza_plugin(stanza.StatelessFileSharing, stanza.Sources)
register_stanza_plugin(
stanza.StatelessFileSharing, self.xmpp["xep_0446"].stanza.File
)
register_stanza_plugin(stanza.Sources, stanza.UrlData, iterable=True)
def get_sfs(
self,
path: Path,
uris: Iterable[str],
media_type: Optional[str],
desc: Optional[str],
):
sfs = stanza.StatelessFileSharing()
sfs["disposition"] = "inline"
for uri in uris:
ref = stanza.UrlData()
ref["target"] = uri
sfs["sources"].append(ref)
if media_type:
sfs["file"]["media-type"] = media_type
if desc:
sfs["file"]["desc"] = desc
sfs["file"]["name"] = path.name
stat = path.stat()
sfs["file"]["size"] = stat.st_size
sfs["file"]["date"] = datetime.fromtimestamp(stat.st_mtime)
h = self.xmpp.plugin["xep_0300"].compute_hash(path)
h["value"] = h["value"].decode()
sfs["file"].append(h)
return sfs

View File

@@ -0,0 +1,21 @@
from slixmpp.xmlstream import ElementBase
NAMESPACE = "urn:xmpp:sfs:0"
class StatelessFileSharing(ElementBase):
name = "file-sharing"
plugin_attrib = "sfs"
namespace = NAMESPACE
interfaces = {"disposition"}
class Sources(ElementBase):
name = plugin_attrib = "sources"
namespace = NAMESPACE
class UrlData(ElementBase):
name = plugin_attrib = "url-data"
namespace = "http://jabber.org/protocol/url-data"
interfaces = {"target"}

View File

@@ -13,7 +13,7 @@ class XEP_0461(BasePlugin):
name = "xep_0461"
description = "XEP-0461: Message Replies"
dependencies = {"xep_0030"}
dependencies = {"xep_0030", "xep_0428"}
stanza = stanza
namespace = stanza.NS

View File

@@ -1,5 +1,8 @@
from typing import Optional
from slixmpp.stanza import Message
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
from slixmpp.plugins.xep_0428.stanza import Fallback
NS = "urn:xmpp:reply:0"
@@ -10,38 +13,44 @@ class Reply(ElementBase):
plugin_attrib = "reply"
interfaces = {"id", "to"}
def add_quoted_fallback(self, fallback: str, nickname: Optional[str] = None):
"""
Add plain text fallback for clients not implementing XEP-0461.
class FeatureFallBack(ElementBase):
# should also be a multi attrib
namespace = "urn:xmpp:feature-fallback:0"
name = "fallback"
plugin_attrib = "feature_fallback"
interfaces = {"for"}
``msg["reply"].add_quoted_fallback("Some text", "Bob")`` will
prepend "> Bob:\n> Some text\n" to the body of the message, and set the
fallback_body attributes accordingly, so that clients implementing
XEP-0461 can hide the fallback text.
def get_stripped_body(self):
# only works for a single fallback_body attrib
start = self["fallback_body"]["start"]
end = self["fallback_body"]["end"]
body = self.parent()["body"]
try:
start = int(start)
end = int(end)
except ValueError:
return body
:param fallback: Body of the quoted message.
:param nickname: Optional, nickname of the quoted participant.
"""
msg = self.parent()
quoted = "\n".join("> " + x.strip() for x in fallback.split("\n")) + "\n"
if nickname:
quoted = "> " + nickname + ":\n" + quoted
msg["body"] = quoted + msg["body"]
fallback = Fallback()
fallback["for"] = NS
fallback["body"]["start"] = 0
fallback["body"]["end"] = len(quoted)
msg.append(fallback)
def get_fallback_body(self) -> str:
msg = self.parent()
for fallback in msg["fallbacks"]:
if fallback["for"] == NS:
break
else:
return body[:start] + body[end:]
class FallBackBody(ElementBase):
# According to https://xmpp.org/extensions/inbox/compatibility-fallback.html
# this should be a multi_attrib *but* since it's a protoXEP, we'll see...
namespace = FeatureFallBack.namespace
name = "body"
plugin_attrib = "fallback_body"
interfaces = {"start", "end"}
return ""
start = fallback["body"]["start"]
end = fallback["body"]["end"]
body = msg["body"]
if start <= end:
return body[start:end]
else:
return ""
def register_plugins():
register_stanza_plugin(Message, Reply)
register_stanza_plugin(Message, FeatureFallBack)
register_stanza_plugin(FeatureFallBack, FallBackBody)

View File

@@ -0,0 +1,8 @@
from slixmpp.plugins.base import register_plugin
from . import stanza
from .pinning import XEP_0469
register_plugin(XEP_0469)
__all__ = ['stanza', 'XEP_0469']

View File

@@ -0,0 +1,17 @@
from slixmpp.plugins import BasePlugin
from . import stanza
class XEP_0469(BasePlugin):
"""
XEP-0469: Bookmark Pinning
"""
name = "xep_0469"
description = "XEP-0469: Bookmark Pinning"
dependencies = {"xep_0402"}
stanza = stanza
def plugin_init(self):
stanza.register_plugin()

View File

@@ -0,0 +1,31 @@
from slixmpp import register_stanza_plugin
from slixmpp.plugins.xep_0402.stanza import Extensions
from slixmpp.xmlstream import ElementBase
NS = "urn:xmpp:bookmarks-pinning:0"
class Pinned(ElementBase):
"""
Pinned bookmark element
To enable it on a Conference element, use enable() like this:
.. code-block::python
# C being a Conference element
C['extensions'].enable('pinned')
Which will add the <pinned> element to the <extensions> element.
"""
namespace = NS
name = "pinned"
plugin_attrib = "pinned"
interfaces = {"pinned"}
bool_interfaces = {"pinned"}
is_extension = True
def register_plugin():
register_stanza_plugin(Extensions, Pinned)

View File

@@ -69,12 +69,14 @@ from slixmpp.plugins.xep_0249 import XEP_0249
from slixmpp.plugins.xep_0256 import XEP_0256
from slixmpp.plugins.xep_0257 import XEP_0257
from slixmpp.plugins.xep_0258 import XEP_0258
from slixmpp.plugins.xep_0264 import XEP_0264
from slixmpp.plugins.xep_0279 import XEP_0279
from slixmpp.plugins.xep_0280 import XEP_0280
from slixmpp.plugins.xep_0297 import XEP_0297
from slixmpp.plugins.xep_0300 import XEP_0300
from slixmpp.plugins.xep_0308 import XEP_0308
from slixmpp.plugins.xep_0313 import XEP_0313
from slixmpp.plugins.xep_0317 import XEP_0317
from slixmpp.plugins.xep_0319 import XEP_0319
from slixmpp.plugins.xep_0332 import XEP_0332
from slixmpp.plugins.xep_0333 import XEP_0333
@@ -100,6 +102,7 @@ from slixmpp.plugins.xep_0428 import XEP_0428
from slixmpp.plugins.xep_0437 import XEP_0437
from slixmpp.plugins.xep_0439 import XEP_0439
from slixmpp.plugins.xep_0444 import XEP_0444
from slixmpp.plugins.xep_0461 import XEP_0461
class PluginsDict(TypedDict):
@@ -162,12 +165,14 @@ class PluginsDict(TypedDict):
xep_0256: XEP_0256
xep_0257: XEP_0257
xep_0258: XEP_0258
xep_0264: XEP_0264
xep_0279: XEP_0279
xep_0280: XEP_0280
xep_0297: XEP_0297
xep_0300: XEP_0300
xep_0308: XEP_0308
xep_0313: XEP_0313
xep_0317: XEP_0317
xep_0319: XEP_0319
xep_0332: XEP_0332
xep_0333: XEP_0333
@@ -193,3 +198,4 @@ class PluginsDict(TypedDict):
xep_0437: XEP_0437
xep_0439: XEP_0439
xep_0444: XEP_0444
xep_0461: XEP_0461

View File

@@ -176,7 +176,7 @@ class Message(RootStanza):
"""
new_message = StanzaBase.reply(self, clear)
if self['type'] == 'groupchat':
if not getattr(self.stream, "is_component", False) and self['type'] == 'groupchat':
new_message['to'] = new_message['to'].bare
new_message['thread'] = self['thread']

View File

@@ -63,6 +63,8 @@ class RootStanza(StanzaBase):
reply['error']['condition'] = e.condition
reply['error']['text'] = e.text
reply['error']['type'] = e.etype
if e.by:
reply["error"]["by"] = e.by
if e.extension is not None:
# Extended error tag
extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension),

View File

@@ -29,9 +29,9 @@ class SlixIntegration(IsolatedAsyncioTestCase):
self.clients = []
self.addAsyncCleanup(self._destroy)
def envjid(self, name):
def envjid(self, name: str, *, default: Optional[str] = None) -> JID:
"""Get a JID from an env var"""
value = os.getenv(name)
value = os.getenv(name, default=default)
return JID(value)
def envstr(self, name):

View File

@@ -3,6 +3,7 @@
# Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
import atexit
import unittest
from queue import Queue
from xml.parsers.expat import ExpatError
@@ -10,11 +11,13 @@ from xml.parsers.expat import ExpatError
from slixmpp.test import TestTransport
from slixmpp import ClientXMPP, ComponentXMPP
from slixmpp.stanza import Message, Iq, Presence
from slixmpp.stanza.error import Error
from slixmpp.xmlstream import ET
from slixmpp.xmlstream import ElementBase
from slixmpp.xmlstream.tostring import tostring, highlight
from slixmpp.xmlstream.matcher import StanzaPath, MatcherId, MatchIDSender
from slixmpp.xmlstream.matcher import MatchXMLMask, MatchXPath
from slixmpp.xmlstream.stanzabase import register_stanza_plugin
import asyncio
@@ -322,6 +325,7 @@ class SlixTest(unittest.TestCase):
if not plugin_config:
plugin_config = {}
self.mode = mode
if mode == 'client':
self.xmpp = ClientXMPP(jid, password,
sasl_mech=sasl_mech,
@@ -740,3 +744,19 @@ class SlixTest(unittest.TestCase):
# Everything matches
return True
def tearDown(self):
self.stream_close()
if getattr(self, "mode", None) == "component":
Error.namespace = 'jabber:client'
for st in Message, Iq, Presence:
register_stanza_plugin(st, Error)
@atexit.register
def cleanup():
try:
loop = asyncio.get_event_loop()
loop.close()
except:
pass

View File

@@ -83,8 +83,35 @@ MAMDefault = Literal['always', 'never', 'roster']
FilterString = Literal['in', 'out', 'out_sync']
__all__ = [
'Protocol', 'TypedDict', 'Literal', 'OptJid', 'JidStr', 'MAMDefault',
'PresenceTypes', 'PresenceShows', 'MessageTypes', 'IqTypes', 'MucRole',
'MucAffiliation', 'FilterString',
ErrorTypes = Literal["modify", "cancel", "auth", "wait", "cancel"]
ErrorConditions = Literal[
"bad-request",
"conflict",
"feature-not-implemented",
"forbidden",
"gone",
"internal-server-error",
"item-not-found",
"jid-malformed",
"not-acceptable",
"not-allowed",
"not-authorized",
"payment-required",
"recipient-unavailable",
"redirect",
"registration-required",
"remote-server-not-found",
"remote-server-timeout",
"resource-constraint",
"service-unavailable",
"subscription-required",
"undefined-condition",
"unexpected-request",
]
__all__ = [
'Protocol', 'TypedDict', 'Literal', 'OptJid', 'OptJidStr', 'JidStr', 'MAMDefault',
'PresenceTypes', 'PresenceShows', 'MessageTypes', 'IqTypes', 'MucRole',
'MucAffiliation', 'FilterString', 'ErrorConditions', 'ErrorTypes'
]

View File

@@ -181,7 +181,7 @@ class SCRAM(Mech):
channel_binding = True
required_credentials = {'username', 'password'}
optional_credentials = {'authzid', 'channel_binding'}
security = {'encrypted', 'unencrypted_scram'}
security = {'tls_version', 'encrypted', 'unencrypted_scram', 'binding_proposed'}
def setup(self, name):
self.use_channel_binding = False
@@ -244,11 +244,15 @@ class SCRAM(Mech):
self.cnonce = bytes(('%s' % random.random())[2:])
gs2_cbind_flag = b'n'
if self.credentials['channel_binding']:
if self.use_channel_binding:
gs2_cbind_flag = b'p=tls-unique'
else:
gs2_cbind_flag = b'y'
if self.security_settings['binding_proposed']:
if self.credentials['channel_binding'] and \
self.use_channel_binding:
if self.security_settings['tls_version'] != 'TLSv1.3':
gs2_cbind_flag = b'p=tls-unique'
else:
gs2_cbind_flag = b'p=tls-exporter'
else:
gs2_cbind_flag = b'y'
authzid = b''
if self.credentials['authzid']:
@@ -280,7 +284,7 @@ class SCRAM(Mech):
raise SASLCancelled('Invalid nonce')
cbind_data = b''
if self.use_channel_binding:
if self.use_channel_binding and self.credentials['channel_binding']:
cbind_data = self.credentials['channel_binding']
cbind_input = self.gs2_header + cbind_data
channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'')

View File

@@ -5,5 +5,5 @@
# We don't want to have to import the entire library
# just to get the version info for setup.py
__version__ = '1.8.3'
__version_info__ = (1, 8, 3)
__version__ = '1.8.5'
__version_info__ = (1, 8, 5)

View File

@@ -10,5 +10,5 @@ from slixmpp.xmlstream.tostring import tostring, highlight
from slixmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT
__all__ = ['JID', 'StanzaBase', 'ElementBase',
'ET', 'StateMachine', 'tostring', 'highlight', 'XMLStream',
'RESPONSE_TIMEOUT']
'ET', 'tostring', 'highlight', 'XMLStream',
'RESPONSE_TIMEOUT', 'register_stanza_plugin']

View File

@@ -1243,7 +1243,7 @@ class ElementBase(object):
self.init_plugin(item.__class__.plugin_multi_attrib)
else:
self.iterables.append(item)
item.parent = weakref.ref(self)
return self
def appendxml(self, xml: ET.Element) -> ElementBase:

View File

@@ -290,8 +290,8 @@ class XMLStream(asyncio.BaseProtocol):
self.xml_depth = 0
self.xml_root = None
self.force_starttls = None
self.disable_starttls = None
self.force_starttls = True
self.disable_starttls = False
self.waiting_queue = asyncio.Queue()
@@ -405,8 +405,9 @@ class XMLStream(asyncio.BaseProtocol):
self.disconnected.set_result(True)
self.disconnected = asyncio.Future()
def connect(self, host: str = '', port: int = 0, use_ssl: Optional[bool] = False,
force_starttls: Optional[bool] = True, disable_starttls: Optional[bool] = False) -> None:
def connect(self, host: str = '', port: int = 0, use_ssl: Optional[bool] = None,
force_starttls: Optional[bool] = None,
disable_starttls: Optional[bool] = None) -> None:
"""Create a new socket and connect to the server.
:param host: The name of the desired server for the connection.
@@ -523,7 +524,7 @@ class XMLStream(asyncio.BaseProtocol):
else:
self.loop.run_until_complete(self.disconnected)
else:
tasks: List[Awaitable] = [asyncio.sleep(timeout)]
tasks: List[Union[asyncio.Task, asyncio.Future]] = [asyncio.Task(asyncio.sleep(timeout))]
if not forever:
tasks.append(self.disconnected)
self.loop.run_until_complete(asyncio.wait(tasks))
@@ -849,6 +850,8 @@ class XMLStream(asyncio.BaseProtocol):
log.debug("Connection error:", exc_info=True)
self.disconnect()
return False
if transp is None:
raise Exception("Transport should not be none")
der_cert = transp.get_extra_info("ssl_object").getpeercert(True)
pem_cert = ssl.DER_cert_to_PEM_cert(der_cert)
self.event('ssl_cert', pem_cert)
@@ -1339,6 +1342,8 @@ class XMLStream(asyncio.BaseProtocol):
passthrough = True
elif data.get_plugin('session', check=True):
passthrough = True
elif data.get_plugin('register', check=True):
passthrough = True
elif isinstance(data, Handshake):
passthrough = True

278
src/lib.rs Normal file
View File

@@ -0,0 +1,278 @@
use pyo3::exceptions::{PyNotImplementedError, PyValueError};
use pyo3::prelude::*;
pyo3::create_exception!(py_jid, InvalidJID, PyValueError, "Raised when attempting to create a JID that does not pass validation.\n\nIt can also be raised if modifying an existing JID in such a way as\nto make it invalid, such trying to remove the domain from an existing\nfull JID while the local and resource portions still exist.");
fn to_exc(err: jid::Error) -> PyErr {
InvalidJID::new_err(err.to_string())
}
/// A representation of a Jabber ID, or JID.
///
/// Each JID may have three components: a user, a domain, and an optional resource. For example:
/// user@domain/resource
///
/// When a resource is not used, the JID is called a bare JID. The JID is a full JID otherwise.
///
/// Raises InvalidJID if the parser rejects it.
#[pyclass(name = "JID", module = "slixmpp.jid")]
struct PyJid {
jid: Option<jid::Jid>,
}
#[pymethods]
impl PyJid {
#[new]
#[pyo3(signature = (jid=None, bare=false))]
fn new(jid: Option<&Bound<'_, PyAny>>, bare: bool) -> PyResult<Self> {
if let Some(jid) = jid {
if let Ok(py_jid) = jid.extract::<PyRef<PyJid>>() {
if bare {
if let Some(py_jid) = &(*py_jid).jid {
Ok(PyJid {
jid: Some(jid::Jid::Bare(py_jid.to_bare())),
})
} else {
Ok(PyJid { jid: None })
}
} else {
Ok(PyJid {
jid: (*py_jid).jid.clone(),
})
}
} else {
let jid: &str = jid.extract()?;
if jid.is_empty() {
Ok(PyJid { jid: None })
} else {
let mut jid = jid::Jid::new(jid).map_err(to_exc)?;
if bare {
jid = jid::Jid::Bare(jid.into_bare())
}
Ok(PyJid { jid: Some(jid) })
}
}
} else {
Ok(PyJid { jid: None })
}
}
/*
// TODO: implement or remove from the API
fn unescape() {
}
*/
#[getter]
fn get_bare(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid.to_bare().to_string(),
}
}
#[setter]
fn set_bare(&mut self, bare: &str) -> PyResult<()> {
let bare = jid::BareJid::new(bare).map_err(to_exc)?;
self.jid = Some(match &self.jid {
Some(jid::Jid::Bare(_)) | None => jid::Jid::Bare(bare),
Some(jid::Jid::Full(jid)) => jid::Jid::Full(bare.with_resource(&jid.resource())),
});
Ok(())
}
#[getter]
fn get_full(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid.to_string(),
}
}
#[setter]
fn set_full(&mut self, full: &str) -> PyResult<()> {
// JID.full = 'domain' is acceptable in slixmpp.
self.jid = Some(jid::Jid::new(full).map_err(to_exc)?);
Ok(())
}
#[getter]
fn get_node(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid
.node_str()
.map(ToString::to_string)
.unwrap_or_else(String::new),
}
}
#[setter]
fn set_node(&mut self, node: &str) -> PyResult<()> {
let node = jid::NodePart::new(node).map_err(to_exc)?;
self.jid = Some(match &self.jid {
Some(jid::Jid::Bare(jid)) => {
jid::Jid::Bare(jid::BareJid::from_parts(Some(&node), &jid.domain()))
}
Some(jid::Jid::Full(jid)) => jid::Jid::Full(jid::FullJid::from_parts(
Some(&node),
&jid.domain(),
&jid.resource(),
)),
None => Err(InvalidJID::new_err("JID.node must apply to a proper JID"))?,
});
Ok(())
}
#[getter]
fn get_domain(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid.domain_str().to_string(),
}
}
#[setter]
fn set_domain(&mut self, domain: &str) -> PyResult<()> {
let domain = jid::DomainPart::new(domain).map_err(to_exc)?;
self.jid = Some(match &self.jid {
Some(jid::Jid::Bare(jid)) => {
jid::Jid::Bare(jid::BareJid::from_parts(jid.node().as_ref(), &domain))
}
Some(jid::Jid::Full(jid)) => jid::Jid::Full(jid::FullJid::from_parts(
jid.node().as_ref(),
&domain,
&jid.resource(),
)),
None => jid::Jid::Bare(jid::BareJid::from_parts(None, &domain)),
});
Ok(())
}
#[getter]
fn get_resource(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid
.resource_str()
.map(ToString::to_string)
.unwrap_or_else(String::new),
}
}
#[setter]
fn set_resource(&mut self, resource: &str) -> PyResult<()> {
let resource = jid::ResourcePart::new(resource).map_err(to_exc)?;
self.jid = Some(match &self.jid {
Some(jid::Jid::Bare(jid)) => jid::Jid::Full(jid.with_resource(&resource)),
Some(jid::Jid::Full(jid)) => jid::Jid::Full(jid::FullJid::from_parts(
jid.node().as_ref(),
&jid.domain(),
&resource,
)),
None => Err(InvalidJID::new_err(
"JID.resource must apply to a proper JID",
))?,
});
Ok(())
}
/// Use the full JID as the string value.
fn __str__(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid.to_string(),
}
}
/// Use the full JID as the representation.
fn __repr__(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid.to_string(),
}
}
/// Two JIDs are equal if they have the same full JID value.
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: pyo3::basic::CompareOp) -> PyResult<bool> {
let other = if let Ok(other) = other.extract::<PyRef<PyJid>>() {
other
} else if other.is_none() {
Bound::new(other.py(), PyJid::new(None, false)?)?.borrow()
} else {
Bound::new(other.py(), PyJid::new(Some(other), false)?)?.borrow()
};
match (&self.jid, &other.jid) {
(None, None) => Ok(true),
(Some(jid), Some(other)) => match op {
pyo3::basic::CompareOp::Eq => Ok(jid == other),
pyo3::basic::CompareOp::Ne => Ok(jid != other),
_ => Err(PyNotImplementedError::new_err(
"Only == and != are implemented",
)),
},
_ => Ok(false),
}
}
/// Hash a JID based on the string version of its full JID.
fn __hash__(&self) -> isize {
if let Some(jid) = &self.jid {
// Use the same algorithm as the Python JID.
let string = jid.to_string();
unsafe { pyo3::ffi::_Py_HashBytes(string.as_ptr() as *const _, string.len() as isize) }
} else {
0
}
}
// Aliases
#[getter]
fn get_user(&self) -> String {
self.get_node()
}
#[setter]
fn set_user(&mut self, user: &str) -> PyResult<()> {
self.set_node(user)
}
#[getter]
fn get_server(&self) -> String {
self.get_domain()
}
#[setter]
fn set_server(&mut self, server: &str) -> PyResult<()> {
self.set_domain(server)
}
#[getter]
fn get_host(&self) -> String {
self.get_domain()
}
#[setter]
fn set_host(&mut self, host: &str) -> PyResult<()> {
self.set_domain(host)
}
#[getter]
fn get_jid(&self) -> String {
self.get_full()
}
#[setter]
fn set_jid(&mut self, jid: &str) -> PyResult<()> {
self.set_full(jid)
}
}
#[pymodule]
#[pyo3(name = "libslixmpp")]
fn py_jid(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyJid>()?;
m.add("InvalidJID", py.get_type_bound::<InvalidJID>())?;
Ok(())
}

View File

@@ -8,9 +8,6 @@ class TestLiveStream(SlixTest):
Test that we can test a live stanza stream.
"""
def tearDown(self):
self.stream_close()
def testClientConnection(self):
"""Test that we can interact with a live ClientXMPP instance."""
self.stream_start(mode='client',

View File

@@ -8,9 +8,6 @@ class TestEvents(SlixTest):
def setUp(self):
self.stream_start()
def tearDown(self):
self.stream_close()
def testEventHappening(self):
"""Test handler working"""
happened = []

View File

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
import unittest
from slixmpp.test import SlixTest
from slixmpp import JID, InvalidJID
from slixmpp.jid import nodeprep
class TestJIDClass(SlixTest):
@@ -192,10 +191,12 @@ class TestJIDClass(SlixTest):
self.assertRaises(InvalidJID, JID, 'test.com/%s' % resource)
self.assertRaises(InvalidJID, JID, 'user@test.com/%s' % resource)
@unittest.skip('Rust')
def testTooLongDomainLabel(self):
domain = ('a' * 64) + '.com'
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainEmptyLabel(self):
domain = 'aaa..bbb.com'
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@@ -216,6 +217,7 @@ class TestJIDClass(SlixTest):
jid3 = JID('%s/resource' % domain)
jid4 = JID('user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainInvalidIPv6NoBrackets(self):
domain = '::1'
@@ -224,6 +226,7 @@ class TestJIDClass(SlixTest):
self.assertRaises(InvalidJID, JID, '%s/resource' % domain)
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainInvalidIPv6MissingBracket(self):
domain = '[::1'
@@ -232,6 +235,7 @@ class TestJIDClass(SlixTest):
self.assertRaises(InvalidJID, JID, '%s/resource' % domain)
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainInvalidIPv6WrongBracket(self):
domain = '[::]1]'
@@ -240,6 +244,7 @@ class TestJIDClass(SlixTest):
self.assertRaises(InvalidJID, JID, '%s/resource' % domain)
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainWithPort(self):
domain = 'example.com:5555'
@@ -248,12 +253,14 @@ class TestJIDClass(SlixTest):
self.assertRaises(InvalidJID, JID, '%s/resource' % domain)
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainWithTrailingDot(self):
domain = 'example.com.'
jid = JID('user@%s/resource' % domain)
self.assertEqual(jid.domain, 'example.com')
@unittest.skip('Rust')
def testDomainWithDashes(self):
domain = 'example.com-'
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@@ -261,21 +268,13 @@ class TestJIDClass(SlixTest):
domain = '-example.com'
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testACEDomain(self):
domain = 'xn--bcher-kva.ch'
jid = JID('user@%s/resource' % domain)
self.assertEqual(jid.domain.encode('utf-8'), b'b\xc3\xbccher.ch')
def testJIDUnescape(self):
jid = JID('here\\27s_a_wild_\\26_\\2fcr%zy\\2f_\\40ddress\\20for\\3a\\3cwv\\3e(\\22IMPS\\22)\\5c@example.com')
ujid = jid.unescape()
self.assertEqual(ujid.local, 'here\'s_a_wild_&_/cr%zy/_@ddress for:<wv>("imps")\\')
jid = JID('blah\\5cfoo\\5c20bar@example.com')
ujid = jid.unescape()
self.assertEqual(ujid.local, 'blah\\foo\\20bar')
def testStartOrEndWithEscapedSpaces(self):
local = ' foo'
self.assertRaises(InvalidJID, JID, '%s@example.com' % local)
@@ -288,9 +287,5 @@ class TestJIDClass(SlixTest):
#self.assertRaises(InvalidJID, JID, '%s@example.com' % '\\20foo2')
#self.assertRaises(InvalidJID, JID, '%s@example.com' % 'bar2\\20')
def testNodePrepIdemptotent(self):
node = 'ᴹᴵᴷᴬᴱᴸ'
self.assertEqual(nodeprep(node), nodeprep(nodeprep(node)))
suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass)

View File

@@ -5,10 +5,6 @@ from slixmpp.xmlstream.stanzabase import ET
class TestIqStanzas(SlixTest):
def tearDown(self):
"""Shutdown the XML stream after testing."""
self.stream_close()
def testSetup(self):
"""Test initializing default Iq values."""
iq = self.Iq()

View File

@@ -0,0 +1,121 @@
import datetime
import unittest
from slixmpp import Iq
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0292 import stanza
REF = """
<iq>
<vcard xmlns='urn:ietf:params:xml:ns:vcard-4.0'>
<fn>
<text>Full Name</text>
</fn>
<n><given>Full</given><surname>Name</surname></n>
<nickname>
<text>some nick</text>
</nickname>
<bday>
<date>1984-05-21</date>
</bday>
<url>
<uri>https://nicoco.fr</uri>
</url>
<note>
<text>About me</text>
</note>
<impp>
<uri>xmpp:test@localhost</uri>
</impp>
<email>
<text>test@gmail.com</text>
</email>
<tel>
<parameters>
<type><text>work</text></type>
</parameters>
<uri>tel:+555</uri>
</tel>
<adr>
<locality>Nice</locality>
<country>France</country>
</adr>
</vcard>
</iq>
"""
class TestVcard(SlixTest):
def test_basic_interfaces(self):
iq = Iq()
x = iq["vcard"]
x["fn"]["text"] = "Full Name"
x["nickname"]["text"] = "some nick"
x["n"]["given"] = "Full"
x["n"]["surname"] = "Name"
x["bday"]["date"] = datetime.date(1984, 5, 21)
x["note"]["text"] = "About me"
x["url"]["uri"] = "https://nicoco.fr"
x["impp"]["uri"] = "xmpp:test@localhost"
x["email"]["text"] = "test@gmail.com"
x["tel"]["uri"] = "tel:+555"
x["tel"]["parameters"]["type_"]["text"] = "work"
x["adr"]["locality"] = "Nice"
x["adr"]["country"] = "France"
self.check(iq, REF, use_values=False)
def test_easy_interface(self):
iq = Iq()
x: stanza.VCard4 = iq["vcard"]
x["full_name"] = "Full Name"
x["given"] = "Full"
x["surname"] = "Name"
x["birthday"] = datetime.date(1984, 5, 21)
x.add_nickname("some nick")
x.add_note("About me")
x.add_url("https://nicoco.fr")
x.add_impp("xmpp:test@localhost")
x.add_email("test@gmail.com")
x.add_tel("+555", "work")
x.add_address("France", "Nice")
self.check(iq, REF, use_values=False)
def test_2_phones(self):
vcard = stanza.VCard4()
tel1 = stanza.Tel()
tel1["parameters"]["type_"]["text"] = "work"
tel1["uri"] = "tel:+555"
tel2 = stanza.Tel()
tel2["parameters"]["type_"]["text"] = "devil"
tel2["uri"] = "tel:+666"
vcard.append(tel1)
vcard.append(tel2)
self.check(
vcard,
"""
<vcard xmlns='urn:ietf:params:xml:ns:vcard-4.0'>
<tel>
<parameters>
<type><text>work</text></type>
</parameters>
<uri>tel:+555</uri>
</tel>
<tel>
<parameters>
<type><text>devil</text></type>
</parameters>
<uri>tel:+666</uri>
</tel>
</vcard>
""",
use_values=False
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestVcard)

View File

@@ -0,0 +1,67 @@
import unittest
from slixmpp import Presence
from slixmpp.test import SlixTest
import slixmpp.plugins.xep_0317 as xep_0317
from slixmpp.plugins.xep_0317 import stanza
class TestStanzaHats(SlixTest):
def setUp(self):
stanza.register_plugin()
def test_create_hats(self):
raw_xml = """
<hats xmlns="urn:xmpp:hats:0">
<hat uri="http://example.com/hats#Teacher" title="Teacher"/>
</hats>
"""
hats = xep_0317.Hats()
hat = xep_0317.Hat()
hat['uri'] = 'http://example.com/hats#Teacher'
hat['title'] = 'Teacher'
hats.append(hat)
self.check(hats, raw_xml, use_values=False)
def test_set_single_hat(self):
presence = Presence()
presence["hats"]["hat"]["uri"] = "test-uri"
presence["hats"]["hat"]["title"] = "test-title"
self.check(
presence, # language=XML
"""
<presence>
<hats xmlns='urn:xmpp:hats:0'>
<hat uri='test-uri' title='test-title'/>
</hats>
</presence>
""",
)
def test_set_multi_hat(self):
presence = Presence()
presence["hats"].add_hats([("uri1", "title1"), ("uri2", "title2")])
self.check(
presence, # language=XML
"""
<presence>
<hats xmlns='urn:xmpp:hats:0'>
<hat uri='uri1' title='title1'/>
<hat uri='uri2' title='title2'/>
</hats>
</presence>
""",
)
def test_get_hats(self):
presence = Presence()
presence["hats"].add_hats([("uri1", "title1"), ("uri2", "title2")])
for i, hat in enumerate(presence["hats"]["hats"], start=1):
self.assertEqual(hat["uri"], f"uri{i}")
self.assertEqual(hat["title"], f"title{i}")
suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaHats)

View File

@@ -1,9 +1,7 @@
import unittest
from slixmpp import Message
from slixmpp.test import SlixTest
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins.xep_0356 import stanza
from slixmpp.plugins.xep_0356 import stanza, permissions
class TestPermissions(SlixTest):
@@ -12,30 +10,57 @@ class TestPermissions(SlixTest):
def testAdvertisePermission(self):
xmlstring = """
<message from='capulet.net' to='pubub.capulet.lit'>
<message from='capulet.lit' to='pubsub.capulet.lit'>
<privilege xmlns='urn:xmpp:privilege:2'>
<perm access='roster' type='both'/>
<perm access='message' type='outgoing'/>
<perm access='presence' type='managed_entity'/>
<perm access='iq' type='both'/>
</privilege>
</message>
"""
msg = self.Message()
msg["from"] = "capulet.net"
msg["to"] = "pubub.capulet.lit"
# This raises AttributeError: 'NoneType' object has no attribute 'use_origin_id'
# msg["id"] = "id"
msg["from"] = "capulet.lit"
msg["to"] = "pubsub.capulet.lit"
for access, type_ in [
("roster", "both"),
("message", "outgoing"),
("presence", "managed_entity"),
("roster", permissions.RosterAccess.BOTH),
("message", permissions.MessagePermission.OUTGOING),
("presence", permissions.PresencePermission.MANAGED_ENTITY),
("iq", permissions.IqPermission.BOTH),
]:
msg["privilege"].add_perm(access, type_)
self.check(msg, xmlstring)
# Should this one work? → # AttributeError: 'Message' object has no attribute 'permission'
# self.assertEqual(msg.permission["roster"], "both")
def testIqPermission(self):
x = stanza.Privilege()
x["access"] = "iq"
ns = stanza.NameSpace()
ns["ns"] = "some_ns"
ns["type"] = "get"
x["perm"]["access"] = "iq"
x["perm"].append(ns)
ns = stanza.NameSpace()
ns["ns"] = "some_other_ns"
ns["type"] = "both"
x["perm"].append(ns)
self.check(
x,
"""
<privilege xmlns='urn:xmpp:privilege:2'>
<perm access='iq'>
<namespace ns='some_ns' type='get' />
<namespace ns='some_other_ns' type='both' />
</perm>
</privilege>
"""
)
nss = set()
for perm in x["perms"]:
for ns in perm["namespaces"]:
nss.add((ns["ns"], ns["type"]))
assert nss == {("some_ns", "get"), ("some_other_ns", "both")}
suite = unittest.TestLoader().loadTestsFromTestCase(TestPermissions)

View File

@@ -23,34 +23,30 @@ class TestSpamReporting(SlixTest):
report = """
<iq type="set">
<block xmlns="urn:xmpp:blocking">
<report xmlns="urn:xmpp:reporting:0">
<spam/>
</report>
<report xmlns="urn:xmpp:reporting:1" reason="urn:xmpp:reporting:spam"/>
</block>
</iq>
"""
iq = self.Iq()
iq['type'] = 'set'
iq['block']['report']['spam'] = True
iq['block']['report']['reason'] = xep_0377.XEP_0377.SPAM
self.check(iq, report)
self.check(iq, report, use_values=False)
def testEnforceOnlyOneSubElement(self):
report = """
<iq type="set">
<block xmlns="urn:xmpp:blocking">
<report xmlns="urn:xmpp:reporting:0">
<abuse/>
</report>
<report xmlns="urn:xmpp:reporting:1" reason="urn:xmpp:reporting:abuse"/>
</block>
</iq>
"""
iq = self.Iq()
iq['type'] = 'set'
iq['block']['report']['spam'] = True
iq['block']['report']['abuse'] = True
self.check(iq, report)
iq['block']['report']['reason'] = xep_0377.XEP_0377.SPAM
iq['block']['report']['reason'] = xep_0377.XEP_0377.ABUSE
self.check(iq, report, use_values=False)
suite = unittest.TestLoader().loadTestsFromTestCase(TestSpamReporting)

View File

@@ -0,0 +1,50 @@
import unittest
from slixmpp.test import SlixTest
from slixmpp.xmlstream import ElementBase
from slixmpp.plugins.xep_0402 import stanza
class Ext1(ElementBase):
name = "ext1"
namespace = "http://ext1"
class Ext2(ElementBase):
name = "ext2"
namespace = "http://ext2"
class TestPepBookmarks(SlixTest):
def setUp(self):
stanza.register_plugin()
def test_bookmarks_extensions(self):
extension1 = Ext1()
extension2 = Ext2()
bookmark = stanza.Conference()
bookmark["password"] = "pass"
bookmark["nick"] = "nick"
bookmark["autojoin"] = False
bookmark["extensions"].append(extension1)
bookmark["extensions"].append(extension2)
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'
autojoin='false'>
<nick>nick</nick>
<password>pass</password>
<extensions>
<ext1 xmlns="http://ext1" />
<ext2 xmlns="http://ext2" />
</extensions>
</conference>
""",
use_values=False
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestPepBookmarks)

View File

@@ -0,0 +1,149 @@
import unittest
from slixmpp import Message
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0428 import stanza
from slixmpp.plugins import xep_0461
from slixmpp.plugins import xep_0444
class TestFallback(SlixTest):
def setUp(self):
stanza.register_plugins()
def testSingleFallbackBody(self):
message = Message()
message["fallback"]["for"] = "ns"
message["fallback"]["body"]["start"] = 0
message["fallback"]["body"]["end"] = 8
self.check(
message, # language=XML
"""
<message>
<fallback xmlns='urn:xmpp:fallback:0' for='ns'>
<body start="0" end="8" />
</fallback>
</message>
""",
)
def testSingleFallbackSubject(self):
message = Message()
message["fallback"]["for"] = "ns"
message["fallback"]["subject"]["start"] = 0
message["fallback"]["subject"]["end"] = 8
self.check(
message, # language=XML
"""
<message>
<fallback xmlns='urn:xmpp:fallback:0' for='ns'>
<subject start="0" end="8" />
</fallback>
</message>
""",
)
def testSingleFallbackWholeBody(self):
message = Message()
message["fallback"]["for"] = "ns"
message["fallback"].enable("body")
self.check(
message, # language=XML
"""
<message>
<fallback xmlns='urn:xmpp:fallback:0' for='ns'>
<body />
</fallback>
</message>
""",
)
def testMultiFallback(self):
message = Message()
f1 = stanza.Fallback()
f1["for"] = "ns1"
f2 = stanza.Fallback()
f2["for"] = "ns2"
message.append(f1)
message.append(f2)
self.check(
message, # language=XML
"""
<message>
<fallback xmlns='urn:xmpp:fallback:0' for='ns1' />
<fallback xmlns='urn:xmpp:fallback:0' for='ns2' />
</message>
""",
)
for i, fallback in enumerate(message["fallbacks"], start=1):
self.assertEqual(fallback["for"], f"ns{i}")
def testStripFallbackPartOfBody(self):
message = Message()
message["body"] = "> quoted\nsome-body"
message["fallback"]["for"] = xep_0461.stanza.NS
message["fallback"]["body"]["start"] = 0
message["fallback"]["body"]["end"] = 9
self.check(
message, # language=XML
"""
<message>
<body>&gt; quoted\nsome-body</body>
<fallback xmlns='urn:xmpp:fallback:0' for='urn:xmpp:reply:0'>
<body start="0" end="9" />
</fallback>
</message>
""",
)
self.assertEqual(
message["fallback"].get_stripped_body(xep_0461.stanza.NS), "some-body"
)
def testStripWholeBody(self):
message = Message()
message["body"] = "> quoted\nsome-body"
message["fallback"]["for"] = "ns"
message["fallback"].enable("body")
self.check(
message, # language=XML
"""
<message>
<body>&gt; quoted\nsome-body</body>
<fallback xmlns='urn:xmpp:fallback:0' for='ns'>
<body />
</fallback>
</message>
""",
)
self.assertEqual(message["fallback"].get_stripped_body("ns"), "")
def testStripMultiFallback(self):
message = Message()
message["body"] = "> huuuuu\n👍"
message["fallback"]["for"] = xep_0461.stanza.NS
message["fallback"]["body"]["start"] = 0
message["fallback"]["body"]["end"] = 9
reaction_fallback = stanza.Fallback()
reaction_fallback["for"] = xep_0444.stanza.NS
reaction_fallback.enable("body")
message.append(reaction_fallback)
self.assertEqual(message["fallback"].get_stripped_body(xep_0461.stanza.NS), "👍")
self.assertEqual(message["fallback"].get_stripped_body(xep_0444.stanza.NS), "")
suite = unittest.TestLoader().loadTestsFromTestCase(TestFallback)

View File

@@ -1,11 +1,13 @@
import unittest
from slixmpp import Message
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0428 import stanza as fallback_stanza
from slixmpp.plugins.xep_0461 import stanza
class TestReply(SlixTest):
def setUp(self):
fallback_stanza.register_plugins()
stanza.register_plugins()
def testReply(self):
@@ -26,23 +28,50 @@ class TestReply(SlixTest):
def testFallback(self):
message = Message()
message["body"] = "12345\nrealbody"
message["feature_fallback"]["for"] = "NS"
message["feature_fallback"]["fallback_body"]["start"] = "0"
message["feature_fallback"]["fallback_body"]["end"] = "6"
message["fallback"]["for"] = "NS"
message["fallback"]["body"]["start"] = 0
message["fallback"]["body"]["end"] = 6
self.check(
message,
"""
<message xmlns="jabber:client">
<body>12345\nrealbody</body>
<fallback xmlns='urn:xmpp:feature-fallback:0' for='NS'>
<fallback xmlns='urn:xmpp:fallback:0' for='NS'>
<body start="0" end="6" />
</fallback>
</message>
""",
)
assert message["feature_fallback"].get_stripped_body() == "realbody"
assert message["fallback"].get_stripped_body("NS") == "realbody"
def testAddFallBackHelper(self):
msg = Message()
msg["body"] = "Great"
msg["reply"].add_quoted_fallback("Anna wrote:\nHi, how are you?")
self.check(
msg, # language=XML
"""
<message xmlns="jabber:client" type="normal">
<body>> Anna wrote:\n> Hi, how are you?\nGreat</body>
<reply xmlns="urn:xmpp:reply:0" />
<fallback xmlns="urn:xmpp:fallback:0" for="urn:xmpp:reply:0">
<body start='0' end='33' />
</fallback>
</message>
"""
)
def testGetFallBackBody(self):
body = "Anna wrote:\nHi, how are you?"
quoted = "> Anna wrote:\n> Hi, how are you?\n"
msg = Message()
msg["body"] = "Great"
msg["reply"].add_quoted_fallback(body)
body2 = msg["reply"].get_fallback_body()
self.assertTrue(body2 == quoted, body2)
suite = unittest.TestLoader().loadTestsFromTestCase(TestReply)

View File

@@ -0,0 +1,36 @@
import unittest
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0469 import stanza
from slixmpp.plugins.xep_0402 import stanza as b_stanza
class TestBookmarksPinning(SlixTest):
def setUp(self):
b_stanza.register_plugin()
stanza.register_plugin()
def test_pinned(self):
bookmark = b_stanza.Conference()
bookmark["password"] = "pass"
bookmark["nick"] = "nick"
bookmark["autojoin"] = False
bookmark["extensions"].enable("pinned")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'
autojoin='false'>
<nick>nick</nick>
<password>pass</password>
<extensions>
<pinned xmlns="urn:xmpp:bookmarks-pinning:0" />
</extensions>
</conference>
""",
use_values=False
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestBookmarksPinning)

View File

@@ -8,9 +8,6 @@ class TestStreamTester(SlixTest):
Test that we can simulate and test a stanza stream.
"""
def tearDown(self):
self.stream_close()
def testClientEcho(self):
"""Test that we can interact with a ClientXMPP instance."""
self.stream_start(mode='client')

View File

@@ -10,9 +10,6 @@ class TestStreamExceptions(SlixTest):
Test handling roster updates.
"""
def tearDown(self):
self.stream_close()
def testExceptionContinueWorking(self):
"""Test that Slixmpp continues to respond after an XMPPError is raised."""

View File

@@ -14,9 +14,6 @@ class TestFilters(SlixTest):
def setUp(self):
self.stream_start()
def tearDown(self):
self.stream_close()
def testIncoming(self):
data = []

View File

@@ -15,9 +15,6 @@ class TestHandlers(SlixTest):
def setUp(self):
self.stream_start()
def tearDown(self):
self.stream_close()
def testCallback(self):
"""Test using stream callback handlers."""

Some files were not shown because too many files have changed in this diff Show More