Compare commits

...

145 Commits

Author SHA1 Message Date
nicoco
e45901064c replace setup.py and mypy.ini with pyproject.toml 2023-07-23 16:04:50 +02:00
nicoco
0b6496a7b1 remove unused CI conf files 2023-07-23 15:34:57 +02: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
Maxime “pep” Buquet
752f4258df Release 1.8.3
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-11-12 21:39:50 +01:00
Maxime “pep” Buquet
b60b1b985d CVE-2022-45197: Fix missing certificate hostname validation
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-11-12 21:36:11 +01:00
Maxime Buquet
e93e43df66 Merge branch 'fix-adhoc-crash' into 'master'
fix crash on adhoc command with bad clients

See merge request poezio/slixmpp!222
2022-11-06 12:55:56 +00:00
nicoco
cfd1af88eb fix crash on adhoc command with bad clients
If a command has no "next" handler, slixmpp
crashes if a client acts as if there was a
next step.
This raises an XMPPError instead
2022-11-06 08:12:37 +01:00
nicoco
65636b8cce implements XEP-0292 (vCard4 over XMPP) 2022-11-04 09:36:25 +01:00
Maxime Buquet
7a0fb97083 Merge branch 'restore-stringprep-warning' into 'master'
logger: remove NullHandler for the "slixmpp" handler

See merge request poezio/slixmpp!220
2022-10-03 08:20:19 +00:00
nicoco
189bbcce19 logger: remove NullHandler for the "slixmpp" handler
This does not seem to accomplish anything besides
hiding the "using the slow, pure python stringprep"
warning, unless you import logging and add another
handler before to the "slixmpp" logger *BEFORE*
importing slixmpp.
2022-10-03 10:16:02 +02:00
Maxime Buquet
79607e43f1 Merge branch 'fix-0084' into 'master'
xep_0084: fix typo and getters

See merge request poezio/slixmpp!219
2022-09-23 08:59:05 +00:00
nicoco
e062181f84 xep_0084: fix typo and getters
"with" instead of "width"; wrong syntax for getters
2022-09-22 23:19:16 +02:00
mathieui
97b0c7ffac Merge branch 'xep0055' into 'master'
Add XEP-0055 (Jabber Search)

See merge request poezio/slixmpp!204
2022-09-12 18:18:51 +00:00
nicoco
c2ece57dee Add XEP-0055 (Jabber Search) 2022-09-11 23:22:44 +02:00
mathieui
afdfa1ee57 Merge branch 'xep0363-as-component' into 'master'
XEP-0363: Fix upload service auto discovery for components

See merge request poezio/slixmpp!207
2022-09-09 16:07:46 +00:00
mathieui
cba5dc7ddc Merge branch 'component-ifrom' into 'master'
xep_0030: fix ifrom for disco queries sent by components

See merge request poezio/slixmpp!216
2022-09-09 16:06:38 +00:00
mathieui
b3a6c7a4ea Merge branch 'aiodns-gethostbyname' into 'master'
Use gethostbyname when using aiodns

See merge request poezio/slixmpp!212
2022-09-09 16:04:14 +00:00
mathieui
11e27d1d7d Merge branch 'mypy-workaround' into 'master'
Fix gitlab pipelines

See merge request poezio/slixmpp!217
2022-09-09 16:02:18 +00:00
nicoco
fbdff30dda fix emoji==2.0.0 compatibility 2022-08-29 00:59:14 +02:00
nicoco
62701bc562 xmlstream: ignore task type (mypy)
This is not satisfying, but having gitlab pipelines running would be nice, wouldn't it?
2022-08-29 00:20:36 +02:00
nicoco
b14918808c xep_0030: fix ifrom for disco queries sent by components
xep_0030 automatically sends disco queries with ifrom=None
Prosody's mod_component had a workaround to allow this non-standard behaviour, but it will change in a future release.
2022-08-29 00:03:55 +02:00
Link Mauve
f5cb9fe66b Merge branch 'doap-sasl-anon' into 'master'
DOAP: Add 0175. It's been here forever

See merge request poezio/slixmpp!215
2022-08-23 09:18:02 +00:00
Maxime “pep” Buquet
8bd53f7098 DOAP: Add 0175. It's been here forever
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-08-23 11:12:26 +02:00
Link Mauve
c955cf1c66 Merge branch 'xep-0461' into 'master'
XEP-0461: Message Replies

See merge request poezio/slixmpp!213
2022-08-21 12:24:08 +00:00
Maxime Buquet
6904ae63f5 Merge branch 'optional-setters' into 'master'
JID: Make node and resource setters accept None

See merge request poezio/slixmpp!214
2022-08-21 12:22:17 +00:00
Emmanuel Gil Peyrot
1caada197a JID: Make node and resource setters accept None
This is the proper way to unset these.
2022-08-21 14:18:53 +02:00
nicoco
450aaa7f86 XEP-0461: Message Replies 2022-08-20 13:35:38 +02:00
Daniel Roschka
d43c83800e Use gethostbyname when using aiodns
Slixmpp behaves differently when resolving host names, whether aiodns
is used or not. With aiodns only DNS is used, while without
`asyncio.loop.getaddrinfo()` is used instead, which utilizes the Name
Service Switch (NSS) to resolve host names by other means (hosts-file,
mDNS, ...) as well.

To unify the behavior, this replaces the use of
`aiodns.DNSResolver().query()` with
`aiodns.DNSResolver().gethostbyname()`. This makes the behavior
resolving host names more consistent between using aiodns or not, as
both now honor the NSS configuration and removes the need for the
previously existing workaround to resolve localhost.
2022-07-31 13:15:25 +02:00
nicoco
14786abd34 Revert "Make it clear that filename does *not* have to be path, and is mandatory"
This reverts commit ed820bf551.
2022-07-16 20:23:48 +02:00
Maxime Buquet
1f47acaec1 Merge branch 'fix-xep_0115-static' into 'master'
XEP-0115: Make get_caps() async

See merge request poezio/slixmpp!203
2022-07-16 19:02:06 +02:00
nicoco
ed820bf551 Make it clear that filename does *not* have to be path, and is mandatory 2022-07-16 17:17:22 +02:00
nicoco
afedfa4b06 Merge branch 'master' of https://lab.louiz.org/poezio/slixmpp 2022-07-16 17:08:21 +02:00
Maxime Buquet
5998069203 Merge branch 'mini_dateutil-no-more' into 'master'
Remove mini_dateutil and replace it with datetime

See merge request poezio/slixmpp!210
2022-07-12 13:39:02 +02:00
Maxime Buquet
356f16f5af Merge branch 'prevent-naive-datetime' into 'master'
XEP-0203: Prevent naïve datetime from being passed

Closes #3471

See merge request poezio/slixmpp!211
2022-07-12 13:38:52 +02:00
Link Mauve
b8f301b26f Merge branch 'affs-outcast-jid' into 'master'
xep_0045: Require JID when setting outcast affiliation

See merge request poezio/slixmpp!188
2022-07-12 13:28:02 +02:00
Link Mauve
ffaeb31219 Merge branch 'nicoco-master-patch-90506' into 'master'
Add xep_0356 to plugins.__all__

See merge request poezio/slixmpp!201
2022-07-12 13:26:54 +02:00
Link Mauve
9560f39de7 Merge branch 'xep0356-v0.4' into 'master'
XEP-0356: namespace version bump

See merge request poezio/slixmpp!206
2022-07-12 13:26:14 +02:00
Link Mauve
f7a38a028a Merge branch 'default-to-CAs' into 'master'
xmlstream: load default CA store by default

See merge request poezio/slixmpp!209
2022-07-12 13:24:32 +02:00
Emmanuel Gil Peyrot
65d70fe417 XEP-0203: Prevent naïve datetime from being passed
The specification says “The format MUST adhere to the dateTime format
specified in XEP-0082 and MUST be expressed in UTC.”

We now respect this requirement, by rejecting naïve datetimes with a
ValueError exception, and converting the passed datetime to UTC.

Fixes #3471.
2022-07-12 13:15:31 +02:00
Emmanuel Gil Peyrot
108a256537 thirdparty: Remove the mini_dateutil module
The builtin datetime module already provides the same features, there is
no need to carry that code any longer.
2022-07-12 12:55:20 +02:00
Emmanuel Gil Peyrot
78a5f79240 XEP-0202: Remove usage of mini_dateutil
Like the previous commit, we now use the builtin datetime module always.
2022-07-12 12:54:35 +02:00
Emmanuel Gil Peyrot
fc63768cfc XEP-0082: Move from mini_dateutil to datetime
Since datetime got merged into Python (probably around py3k), it’s now
usable for all of our needs and so we can do away with the old fallback.
2022-07-12 12:51:22 +02:00
Maxime “pep” Buquet
90e79af18a xmlstream: load default CA store by default
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-07-11 14:46:00 +02:00
Link Mauve
5e5a741994 Merge branch 'dns-reconnect' into 'master'
Fix delayed reconnect after DNS failure

See merge request poezio/slixmpp!208
2022-06-22 11:50:12 +02:00
Georg Lukas
b44ab17c8f Fix delayed reconnect after DNS failure
The XML stream will re-schedule a reconnect on socket errors, except
for DNS failures. If a user has no uplink connection, then DNS will
also fail, preventing an automatic reconnection.

This patch consolidates the two code paths and sets a maximum back-off
time of 5min (300s).
2022-06-22 11:39:44 +02:00
Nicolas Cedilnik
afb5419b68 XEP-0363: Fix upload service auto discovery for components 2022-06-18 06:09:36 +02:00
Nicolas Cedilnik
a1a5f3984d XEP-0356: namespace version bump 2022-06-09 16:45:36 +02:00
Nicolas Cedilnik
8eb8769862 XEP-0115: Make get_caps() async 2022-06-09 15:33:02 +02:00
Link Mauve
5ceb48bbcd Merge branch 'origin-id-non-default' into 'master'
Change origin-id defaults to False

See merge request poezio/slixmpp!202
2022-05-28 17:44:42 +02:00
Maxime “pep” Buquet
916894ab7c Change origin-id defaults to False
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-05-28 13:50:09 +02:00
Nicoco K
2b45c22fcb Add xep_0356 to plugins.__all__ 2022-05-19 14:40:45 +02:00
mathieui
566e7dc771 Merge branch 'nicoco-master-patch-38938' into 'master'
Fix typo in chat markers (fixes #3469)

Closes #3469

See merge request poezio/slixmpp!199
2022-05-15 16:44:10 +02:00
Nicoco K
aa492f905c Fix typo in chat markers (fixes #3469) 2022-05-15 07:48:00 +02:00
mathieui
e1a240ec6c Merge branch 'release-version-1.8.2' into 'master'
Update version to 1.8.2

See merge request poezio/slixmpp!197
2022-04-06 22:44:40 +02:00
mathieui
771839242c Update version to 1.8.2 2022-04-06 22:41:40 +02:00
mathieui
8bac744009 Merge branch 'starttls-exception' into 'master'
features_starttls/Proceed: raise exception on InvalidCABundle

See merge request poezio/slixmpp!196
2022-04-05 20:15:49 +02:00
Maxime “pep” Buquet
88d2f5dae4 features_starttls/Proceed: raise exception on InvalidCABundle
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-04-05 19:42:49 +02:00
mathieui
f7902d056e Merge branch 'exn-invalidcabundle-arg' into 'master'
Pass in useful value when raising InvalidCABundle

See merge request poezio/slixmpp!195
2022-04-05 19:42:06 +02:00
Maxime “pep” Buquet
41afbb10df Pass in useful value when raising InvalidCABundle
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-04-05 01:24:14 +02:00
mathieui
aca4addb9c Merge branch 'fix-old-session' into 'master'
stream features: fix old "session" establishment

Closes #3468

See merge request poezio/slixmpp!193
2022-04-01 21:01:31 +02:00
mathieui
914ce40fd5 stream features: fix old "session" establishment
As it is and old and deprecated code path, nobody noticed that it was
broken by the new filtering code.

Fix #3468
2022-04-01 20:56:02 +02:00
Maxime Buquet
82ff68cfac Merge branch 'upload-encrypt' into 'master'
XEP-0454: OMEMO Media Sharing

See merge request poezio/slixmpp!189
2022-03-21 17:01:40 +01:00
Maxime “pep” Buquet
28d44ecf74 xep_0454: str.removeprefix is available since 3.9
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-20 21:34:55 +01:00
Maxime “pep” Buquet
06e4e480c1 xep_0454: keep original filename extension if available
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-20 01:02:14 +01:00
Maxime “pep” Buquet
82ee250295 xep_0454: use staticmethods where possible
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-20 01:02:14 +01:00
Maxime “pep” Buquet
53d38a8115 setup.py: add cryptography in extras_require; update example
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-20 01:02:14 +01:00
Maxime “pep” Buquet
0fba8fd7f8 doap: add 454 entry
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
b899baabd8 xep_0454: also include finalize's result in the payload
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
acad41f3b7 xep_0454: Don't force content-type to application/octect-stream
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
bde5aaaf3e examples/http_upload.py: Add --encrypt parameter to send encrypted files
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
7222ade0dd xep_0454: Ensure format_url returns a str
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
14a6c7801d tests: XEP-0454
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
b52540e49f xep_0454: implement decrypt method
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
c1aeab328b xep_0454: use streaming API from CipherContext
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
51644e301b xep_0454: Add wrapper to xep_363's upload_file
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
bc8af3cc61 xep_0454: new plugin. OMEMO Media Sharing
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
3c08f471cf xep_0363: change filename to Path
This shouldn't break anything as I'm not using Path specific APIs

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
54b724c28b examples/http_upload: Add some typing
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-19 10:31:34 +01:00
Maxime “pep” Buquet
60df4ef7aa xep_0045: Require JID when setting outcast affiliation
Found out when reading poezio/poezio#3536.

“An admin or owner can ban one or more users from a room. The ban MUST
be performed based on the occupant's bare JID.”

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-16 16:12:20 +01:00
114 changed files with 2397 additions and 672 deletions

View File

@@ -12,15 +12,15 @@ 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
- pip3 install emoji aiohttp
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
test-3.10:
@@ -30,34 +30,45 @@ test-3.10:
image: python:3.10
script:
- apt update
- apt install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
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
- pip3 install emoji aiohttp
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
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

View File

@@ -1,7 +0,0 @@
language: python
python:
- "3.7"
- "3.8-dev"
install:
- "pip install ."
script: testall.py

View File

@@ -5,7 +5,7 @@ To contribute, the preferred way is to commit your changes on some
publicly-available git repository (on a fork `on github
<https://github.com/poezio/slixmpp>`_ or on your own repository) and to
notify the developers with either:
- a ticket `on the bug tracker <https://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>`_

50
_custom_build.py Normal file
View File

@@ -0,0 +1,50 @@
"""
Cythonize the .pyx file for sdist, and compile it for wheels.
NB: produced wheels are not valid, but the sdist should be.
"""
import os
from Cython.Build import cythonize
from subprocess import check_output, CalledProcessError, DEVNULL, call
from tempfile import TemporaryFile
from setuptools.command.build_py import build_py
def check_include(library_name, header):
command = [os.environ.get("PKG_CONFIG", "pkg-config"), "--cflags", library_name]
try:
cflags = check_output(command).decode("utf-8").split()
except FileNotFoundError:
print("pkg-config not found.")
return False
except CalledProcessError:
# pkg-config already prints the missing libraries on stderr.
return False
command = [os.environ.get("CC", "cc")] + cflags + ["-E", "-"]
with TemporaryFile("w+") as c_file:
c_file.write("#include <%s>" % header)
c_file.seek(0)
try:
return call(command, stdin=c_file, stdout=DEVNULL, stderr=DEVNULL) == 0
except FileNotFoundError:
print("%s headers not found." % library_name)
return False
class Build(build_py):
def run(self):
self.run_command("build_ext")
return super().run()
def initialize_options(self):
super().initialize_options()
has_python_headers = check_include("python3", "Python.h")
has_stringprep_headers = check_include("libidn", "stringprep.h")
if has_python_headers and has_stringprep_headers:
self.distribution.ext_modules = cythonize("slixmpp/stringprep.pyx")
else:
print("Falling back to the slow stringprep module.")

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>
@@ -455,6 +455,14 @@
<xmpp:since>1.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0175.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2</xmpp:version>
<xmpp:since>1.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/>
@@ -776,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>
@@ -892,6 +900,15 @@
<xmpp:since>1.6.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0454.html"/>
<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>
<release>
<Version>
@@ -995,35 +1012,56 @@
<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://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://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>
</Project>

View File

@@ -0,0 +1,18 @@
XEP-0055: Jabber search
=======================
.. module:: slixmpp.plugins.xep_0055
.. autoclass:: XEP_0055
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0055.stanza
:members:
:undoc-members:

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+,

View File

@@ -5,11 +5,16 @@
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from typing import Optional
import sys
import logging
from pathlib import Path
from getpass import getpass
from argparse import ArgumentParser
import slixmpp
from slixmpp import JID
from slixmpp.exceptions import IqTimeout
log = logging.getLogger(__name__)
@@ -21,20 +26,40 @@ class HttpUpload(slixmpp.ClientXMPP):
A basic client asking an entity if they confirm the access to an HTTP URL.
"""
def __init__(self, jid, password, recipient, filename, domain=None):
def __init__(
self,
jid: JID,
password: str,
recipient: JID,
filename: Path,
domain: Optional[JID] = None,
encrypted: bool = False,
):
slixmpp.ClientXMPP.__init__(self, jid, password)
self.recipient = recipient
self.filename = filename
self.domain = domain
self.encrypted = encrypted
self.add_event_handler("session_start", self.start)
async def start(self, event):
log.info('Uploading file %s...', self.filename)
try:
url = await self['xep_0363'].upload_file(
self.filename, domain=self.domain, timeout=10
upload_file = self['xep_0363'].upload_file
if self.encrypted and not self['xep_0454']:
print(
'The xep_0454 module isn\'t available. '
'Ensure you have \'cryptography\' '
'from extras_require installed.',
file=sys.stderr,
)
return
elif self.encrypted:
upload_file = self['xep_0454'].upload_file
url = await upload_file(
self.filename, domain=self.domain, timeout=10,
)
except IqTimeout:
raise TimeoutError('Could not send message in time')
@@ -79,6 +104,10 @@ if __name__ == '__main__':
parser.add_argument("--domain",
help="Domain to use for HTTP File Upload (leave out for your own servers)")
parser.add_argument("-e", "--encrypt", dest="encrypted",
help="Whether to encrypt", action="store_true",
default=False)
args = parser.parse_args()
# Setup logging.
@@ -86,15 +115,41 @@ if __name__ == '__main__':
format='%(levelname)-8s %(message)s')
if args.jid is None:
args.jid = input("Username: ")
args.jid = JID(input("Username: "))
if args.password is None:
args.password = getpass("Password: ")
xmpp = HttpUpload(args.jid, args.password, args.recipient, args.file, args.domain)
domain = args.domain
if domain is not None:
domain = JID(domain)
if args.encrypted:
print(
'You are using the --encrypt flag. '
'Be aware that the transport being used is NOT end-to-end '
'encrypted. The server will be able to decrypt the file.',
file=sys.stderr,
)
xmpp = HttpUpload(
jid=args.jid,
password=args.password,
recipient=JID(args.recipient),
filename=Path(args.file),
domain=domain,
encrypted=args.encrypted,
)
xmpp.register_plugin('xep_0066')
xmpp.register_plugin('xep_0071')
xmpp.register_plugin('xep_0128')
xmpp.register_plugin('xep_0363')
try:
xmpp.register_plugin('xep_0454')
except slixmpp.plugins.base.PluginNotFound:
log.error(
'Could not load xep_0454. '
'Ensure you have \'cryptography\' from extras_require installed.'
)
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()

View File

@@ -1,15 +0,0 @@
[mypy]
check_untyped_defs = False
ignore_missing_imports = True
[mypy-slixmpp.types]
ignore_errors = True
[mypy-slixmpp.thirdparty.*]
ignore_errors = True
[mypy-slixmpp.plugins.*]
ignore_errors = True
[mypy-slixmpp.plugins.base]
ignore_errors = False

67
pyproject.toml Normal file
View File

@@ -0,0 +1,67 @@
[project]
name = "slixmpp"
version = "1.8.4"
description = 'Slixmpp is an elegant Python library for XMPP (aka Jabber).'
requires-python = ">=3.7"
dependencies = [
"aiodns >= 1.0",
"pyasn1",
"pyasn1_modules",
"typing_extensions; python_version < '3.8.0'",
]
classifiers = [
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Topic :: Internet :: XMPP',
'Topic :: Software Development :: Libraries :: Python Modules',
]
readme = "README.rst"
license = { file = "LICENSE" }
[[project.authors]]
name = "Florent Le Coz"
email = "louiz@louiz.org"
[project.urls]
Repository = 'https://codeberg.org/poezio/slixmpp'
[project.optional-dependencies]
XEP-0363 = ['aiohttp']
XEP-0444 = ['emoji']
XEP-0454 = ['cryptography']
Safer-XML-parsing = ['defusedxml']
[build-system]
requires = ["setuptools", "cython"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["slixmpp"]
py-modules = ["_custom_build"]
[tool.setuptools.cmdclass]
build_py = "_custom_build.Build"
[tool.mypy]
check_untyped_defs = false
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = 'slixmpp.types'
ignore_errors = true
[[tool.mypy.overrides]]
module = 'slixmpp.thirdparty.*'
ignore_errors = true
[[tool.mypy.overrides]]
module = 'slixmpp.plugins.*'
ignore_errors = true
[[tool.mypy.overrides]]
module = 'slixmpp.plugins.base'
ignore_errors = false

View File

@@ -1,97 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2011 Nathanael C. Fritz
# All Rights Reserved
#
# This software is licensed as described in the README.rst and LICENSE
# file, which you should have received as part of this distribution.
import runpy
import os
from pathlib import Path
from subprocess import call, DEVNULL, check_output, CalledProcessError
from tempfile import TemporaryFile
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
from run_tests import TestCommand
version_mod = runpy.run_path('slixmpp/version.py')
VERSION = version_mod['__version__']
DESCRIPTION = ('Slixmpp is an elegant Python library for XMPP (aka Jabber).')
with open('README.rst', encoding='utf8') as readme:
LONG_DESCRIPTION = readme.read()
CLASSIFIERS = [
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Topic :: Internet :: XMPP',
'Topic :: Software Development :: Libraries :: Python Modules',
]
packages = [str(mod.parent) for mod in Path('slixmpp').rglob('__init__.py')]
def check_include(library_name, header):
command = [os.environ.get('PKG_CONFIG', 'pkg-config'), '--cflags', library_name]
try:
cflags = check_output(command).decode('utf-8').split()
except FileNotFoundError:
print('pkg-config not found.')
return False
except CalledProcessError:
# pkg-config already prints the missing libraries on stderr.
return False
command = [os.environ.get('CC', 'cc')] + cflags + ['-E', '-']
with TemporaryFile('w+') as c_file:
c_file.write('#include <%s>' % header)
c_file.seek(0)
try:
return call(command, stdin=c_file, stdout=DEVNULL, stderr=DEVNULL) == 0
except FileNotFoundError:
print('%s headers not found.' % library_name)
return False
HAS_PYTHON_HEADERS = check_include('python3', 'Python.h')
HAS_STRINGPREP_HEADERS = check_include('libidn', 'stringprep.h')
ext_modules = None
if HAS_PYTHON_HEADERS and HAS_STRINGPREP_HEADERS:
try:
from Cython.Build import cythonize
except ImportError:
print('Cython not found, falling back to the slow stringprep module.')
else:
ext_modules = cythonize('slixmpp/stringprep.pyx')
else:
print('Falling back to the slow stringprep module.')
setup(
name="slixmpp",
version=VERSION,
description=DESCRIPTION,
long_description=LONG_DESCRIPTION,
author='Florent Le Coz',
author_email='louiz@louiz.org',
url='https://lab.louiz.org/poezio/slixmpp',
license='MIT',
platforms=['any'],
package_data={'slixmpp': ['py.typed']},
packages=packages,
ext_modules=ext_modules,
install_requires=['aiodns>=1.0', 'pyasn1', 'pyasn1_modules', 'typing_extensions; python_version < "3.8.0"'],
extras_require={
'XEP-0363': ['aiohttp'],
'XEP-0444 compliance': ['emoji'],
'Safer XML parsing': ['defusedxml'],
},
classifiers=CLASSIFIERS,
cmdclass={'test': TestCommand}
)

View File

@@ -5,7 +5,6 @@
# See the file LICENSE for copying permission.
import logging
from os import getenv
logging.getLogger(__name__).addHandler(logging.NullHandler())
# Use defusedxml if wanted
# Since enabling it can have adverse consequences for the programs using

View File

@@ -140,7 +140,7 @@ class BaseXMPP(XMLStream):
self.use_presence_ids = True
#: XEP-0359 <origin-id/> tag that gets added to <message/> stanzas.
self.use_origin_id = True
self.use_origin_id = False
#: The API registry is a way to process callbacks based on
#: JID+node combinations. Each callback in the registry is
@@ -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

@@ -9,13 +9,14 @@
import logging
import hashlib
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 +40,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 +62,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,6 +88,11 @@ class ComponentXMPP(BaseXMPP):
self.add_event_handler('presence_probe',
self._handle_probe)
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=None, port=None, use_ssl=False):
"""Connect to the server.

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

@@ -3,8 +3,12 @@
# Copyright (C) 2011 Nathanael C. Fritz
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from slixmpp.xmlstream import StanzaBase, ElementBase
from typing import Set, ClassVar
from slixmpp.xmlstream import StanzaBase, ElementBase
from slixmpp.xmlstream.xmlstream import InvalidCABundle
import logging
log = logging.getLogger(__name__)
class STARTTLS(StanzaBase):
@@ -36,6 +40,12 @@ class Proceed(StanzaBase):
namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
interfaces: ClassVar[Set[str]] = set()
def exception(self, e: Exception) -> None:
log.exception('Error handling {%s}%s stanza',
self.namespace, self.name)
if isinstance(e, InvalidCABundle):
raise e
class Failure(StanzaBase):
"""

View File

@@ -303,13 +303,15 @@ class JID:
:param string jid:
A string of the form ``'[user@]domain[/resource]'``.
:param bool bare:
If present, discard the provided resource.
:raises InvalidJID:
"""
__slots__ = ('_node', '_domain', '_resource', '_bare', '_full')
def __init__(self, jid: Optional[Union[str, 'JID']] = None):
def __init__(self, jid: Optional[Union[str, 'JID']] = None, bare: bool = False):
if not jid:
self._node = ''
self._domain = ''
@@ -318,11 +320,14 @@ class JID:
self._full = ''
return
elif not isinstance(jid, JID):
self._node, self._domain, self._resource = _parse_jid(jid)
node, domain, resource = _parse_jid(jid)
self._node = node
self._domain = domain
self._resource = resource if not bare else ''
else:
self._node = jid._node
self._domain = jid._domain
self._resource = jid._resource
self._resource = jid._resource if not bare else ''
self._update_bare_full()
def unescape(self):
@@ -368,7 +373,7 @@ class JID:
return self._node
@node.setter
def node(self, value: str):
def node(self, value: Optional[str]):
self._node = _validate_node(value)
self._update_bare_full()
@@ -386,7 +391,7 @@ class JID:
return self._resource
@resource.setter
def resource(self, value: str):
def resource(self, value: Optional[str]):
self._resource = _validate_resource(value)
self._update_bare_full()

View File

@@ -1,4 +1,3 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2010 Nathanael C. Fritz
# This file is part of Slixmpp.
@@ -7,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
@@ -24,6 +23,7 @@ __all__ = [
'xep_0049', # Private XML Storage
'xep_0050', # Ad-hoc Commands
'xep_0054', # vcard-temp
'xep_0055', # Jabber Search
'xep_0059', # Result Set Management
'xep_0060', # Pubsub (Client)
'xep_0065', # SOCKS5 Bytestreams
@@ -79,6 +79,7 @@ __all__ = [
# '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
@@ -93,13 +94,16 @@ __all__ = [
'xep_0335', # JSON Containers
'xep_0352', # Client State Indication
'xep_0353', # Jingle Message Initiation
'xep_0356', # Privileged entity
'xep_0359', # Unique and Stable Stanza IDs
'xep_0363', # HTTP File Upload
'xep_0369', # MIX-CORE
'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
@@ -112,4 +116,15 @@ __all__ = [
'xep_0439', # Quick Response
'xep_0441', # Message Archive Management Preferences
'xep_0444', # Message Reactions
'xep_0447', # Stateless file sharing
'xep_0461', # Message Replies
# 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,
@@ -385,6 +385,8 @@ class XEP_0030(BasePlugin):
local = True
ifrom = kwargs.pop('ifrom', None)
if self.xmpp.is_component and ifrom is None:
ifrom = self.xmpp.boundjid
if local:
log.debug("Looking up local disco#info data "
"for %s, node %s.", jid, node)
@@ -455,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)
@@ -493,6 +492,8 @@ class XEP_0045(BasePlugin):
"""
if affiliation not in AFFILIATIONS:
raise ValueError('%s is not a valid affiliation' % affiliation)
if affiliation == 'outcast' and not jid:
raise ValueError('Outcast affiliation requires a using a jid')
if not any((jid, nick)):
raise ValueError('One of jid or nick must be set')
iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)

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,8 +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 asyncio.iscoroutinefunction(handler):
if handler is None:
raise XMPPError("bad-request", text="The command is completed")
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

@@ -0,0 +1,6 @@
from slixmpp.plugins.base import register_plugin
from .search import XEP_0055
register_plugin(XEP_0055)

View File

@@ -0,0 +1,89 @@
import logging
from slixmpp import CoroutineCallback, StanzaPath, Iq, register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream import StanzaBase
from . import stanza
class XEP_0055(BasePlugin):
"""
XEP-0055: Jabber Search
The config options are only useful for a "server-side" search feature,
and if the ``provide_search`` option is set to True.
API
===
``search_get_form``: customize the search form content (ie fields)
``search_query``: return search results
"""
name = "xep_0055"
description = "XEP-0055: Jabber search"
dependencies = {"xep_0004", "xep_0030"}
stanza = stanza
default_config = {
"form_fields": {"first", "last"},
"form_instructions": "",
"form_title": "",
"provide_search": True
}
def plugin_init(self):
register_stanza_plugin(Iq, stanza.Search)
register_stanza_plugin(stanza.Search, self.xmpp["xep_0004"].stanza.Form)
if self.provide_search:
self.xmpp["xep_0030"].add_feature(stanza.Search.namespace)
self.xmpp.register_handler(
CoroutineCallback(
"search",
StanzaPath("/iq/search"),
self._handle_search,
)
)
self.api.register(self._get_form, "search_get_form")
self.api.register(self._get_results, "search_query")
async def _handle_search(self, iq: StanzaBase):
if iq["search"]["form"].get_values():
reply = await self.api["search_query"](None, None, iq.get_from(), iq)
reply["search"]["form"]["type"] = "result"
else:
reply = await self.api["search_get_form"](None, None, iq.get_from(), iq)
reply["search"]["form"].add_field(
"FORM_TYPE", value=stanza.Search.namespace, ftype="hidden"
)
reply.send()
async def _get_form(self, jid, node, ifrom, iq):
reply = iq.reply()
form = reply["search"]["form"]
form["title"] = self.form_title
form["instructions"] = self.form_instructions
for field in self.form_fields:
form.add_field(field)
return reply
async def _get_results(self, jid, node, ifrom, iq):
reply = iq.reply()
form = reply["search"]["form"]
form["type"] = "result"
for field in self.form_fields:
form.add_reported(field)
return reply
def make_search_iq(self, **kwargs):
iq = self.xmpp.make_iq(itype="set", **kwargs)
iq["search"]["form"].set_type("submit")
iq["search"]["form"].add_field(
"FORM_TYPE", value=stanza.Search.namespace, ftype="hidden"
)
return iq
log = logging.getLogger(__name__)

View File

@@ -0,0 +1,10 @@
from typing import Set, ClassVar
from slixmpp.xmlstream import ElementBase
class Search(ElementBase):
namespace = "jabber:iq:search"
name = "query"
plugin_attrib = "search"
interfaces: ClassVar[Set[str]] = set()

View File

@@ -6,7 +6,6 @@
import datetime as dt
from slixmpp.plugins import BasePlugin, register_plugin
from slixmpp.thirdparty import tzutc, tzoffset, parse_iso
# =====================================================================
@@ -21,7 +20,10 @@ def parse(time_str):
Arguments:
time_str -- A formatted timestamp string.
"""
return parse_iso(time_str)
try:
return dt.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S.%f%z')
except ValueError:
return dt.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S%z')
def format_date(time_obj):
@@ -52,7 +54,7 @@ def format_time(time_obj):
if isinstance(time_obj, dt.datetime):
time_obj = time_obj.timetz()
timestamp = time_obj.isoformat()
if time_obj.tzinfo == tzutc():
if time_obj.tzinfo == dt.timezone.utc:
timestamp = timestamp[:-6]
return '%sZ' % timestamp
return timestamp
@@ -69,7 +71,7 @@ def format_datetime(time_obj):
time_obj -- A datetime object.
"""
timestamp = time_obj.isoformat('T')
if time_obj.tzinfo == tzutc():
if time_obj.tzinfo == dt.timezone.utc:
timestamp = timestamp[:-6]
return '%sZ' % timestamp
return timestamp
@@ -128,9 +130,9 @@ def time(hour=None, min=None, sec=None, micro=None, offset=None, obj=False):
if micro is None:
micro = now.microsecond
if offset in (None, 0):
offset = tzutc()
offset = dt.timezone.utc
elif not isinstance(offset, dt.tzinfo):
offset = tzoffset(None, offset)
offset = dt.timezone(dt.timedelta(seconds=offset))
value = dt.time(hour, min, sec, micro, offset)
if obj:
return value
@@ -175,9 +177,9 @@ def datetime(year=None, month=None, day=None, hour=None,
if micro is None:
micro = now.microsecond
if offset in (None, 0):
offset = tzutc()
offset = dt.timezone.utc
elif not isinstance(offset, dt.tzinfo):
offset = tzoffset(None, offset)
offset = dt.timezone(dt.timedelta(seconds=offset))
value = dt.datetime(year, month, day, hour,
min, sec, micro, offset)

View File

@@ -80,16 +80,16 @@ class Info(ElementBase):
self._set_int('bytes', value)
def get_height(self) -> int:
self._get_int('height')
return self._get_int('height')
def set_height(self, value: int):
self._set_int('height', value)
def get_width(self) -> int:
self._get_int(self, 'width')
return self._get_int('width')
def set_width(self, value: int):
self._set_int('with', value)
self._set_int('width', value)
class Pointer(ElementBase):

View File

@@ -162,7 +162,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

@@ -60,7 +60,7 @@ class StaticCaps(object):
return False
if node in (None, ''):
info = self.caps.get_caps(jid)
info = await self.caps.get_caps(jid)
if info and feature in info['features']:
return True
@@ -134,7 +134,7 @@ class StaticCaps(object):
def get_verstring(self, jid, node, ifrom, data):
return self.jid_vers.get(jid, None)
def get_caps(self, jid, node, ifrom, data):
async def get_caps(self, jid, node, ifrom, data):
verstring = data.get('verstring', None)
if verstring is None:
return None

View File

@@ -8,7 +8,6 @@ import datetime as dt
from slixmpp.xmlstream import ElementBase
from slixmpp.plugins import xep_0082
from slixmpp.thirdparty import tzutc, tzoffset
class EntityTime(ElementBase):
@@ -87,7 +86,7 @@ class EntityTime(ElementBase):
seconds (positive or negative) to offset.
"""
time = xep_0082.time(offset=value)
if xep_0082.parse(time).tzinfo == tzutc():
if xep_0082.parse(time).tzinfo == dt.timezone.utc:
self._set_sub_text('tzo', 'Z')
else:
self._set_sub_text('tzo', time[-6:])
@@ -111,6 +110,6 @@ class EntityTime(ElementBase):
date = value
if not isinstance(value, dt.datetime):
date = xep_0082.parse(value)
date = date.astimezone(tzutc())
date = date.astimezone(dt.timezone.utc)
value = xep_0082.format_datetime(date)
self._set_sub_text('utc', value)

View File

@@ -30,6 +30,10 @@ class Delay(ElementBase):
def set_stamp(self, value):
if isinstance(value, dt.datetime):
if value.tzinfo is None:
raise ValueError(f'Datetime provided without timezone information: {value}')
if value.tzinfo != dt.timezone.utc:
value = value.astimezone(dt.timezone.utc)
value = xep_0082.format_datetime(value)
self._set_attr('stamp', value)

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 . 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

@@ -187,7 +187,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

@@ -1,4 +1,3 @@
# slixmpp: The Slick XMPP Library
# Copyright (C) 2016 Emmanuel Gil Peyrot
# This file is part of slixmpp.
@@ -68,11 +67,11 @@ class XEP_0333(BasePlugin):
:param JID mto: recipient of the marker
:param str id: Identifier of the marked message
:param str marker: Marker to send (one of
displayed, retrieved, or acknowledged)
displayed, received, or acknowledged)
:param str thread: Message thread
:param str mfrom: Use a specific JID to send the message
"""
if marker not in ('displayed', 'retrieved', 'acknowledged'):
if marker not in ('displayed', 'received', 'acknowledged'):
raise ValueError('Invalid marker: %s' % marker)
msg = self.xmpp.make_message(mto=mto, mfrom=mfrom)
if thread:

View File

@@ -7,7 +7,7 @@ from slixmpp.plugins.xep_0297 import Forwarded
class Privilege(ElementBase):
namespace = "urn:xmpp:privilege:1"
namespace = "urn:xmpp:privilege:2"
name = "privilege"
plugin_attrib = "privilege"
@@ -24,7 +24,10 @@ class Privilege(ElementBase):
def presence(self):
return self.permission("presence")
def iq(self):
return self.permission("iq")
def add_perm(self, access, type):
# This should only be needed for servers, so maybe out of scope for slixmpp
perm = Perm()
@@ -34,7 +37,7 @@ class Privilege(ElementBase):
class Perm(ElementBase):
namespace = "urn:xmpp:privilege:1"
namespace = "urn:xmpp:privilege:2"
name = "perm"
plugin_attrib = "perm"
plugin_multi_attrib = "perms"
@@ -44,4 +47,4 @@ class Perm(ElementBase):
def register():
register_stanza_plugin(Message, Privilege)
register_stanza_plugin(Privilege, Forwarded)
register_stanza_plugin(Privilege, Perm, iterable=True)
register_stanza_plugin(Privilege, Perm, iterable=True)

View File

@@ -14,6 +14,8 @@ from typing import (
IO,
)
from pathlib import Path
from slixmpp import JID, __version__
from slixmpp.stanza import Iq
from slixmpp.plugins import BasePlugin
@@ -99,12 +101,17 @@ class XEP_0363(BasePlugin):
:param domain: Domain to disco to find a service.
"""
if domain is None and self.xmpp.is_component:
domain = self.xmpp.server_host
results = await self.xmpp['xep_0030'].get_info_from_domain(
domain=domain, **iqkwargs
)
candidates = []
for info in results:
if not info['disco_info']:
continue
for identity in info['disco_info']['identities']:
if identity[0] == 'store' and identity[1] == 'file':
candidates.append(info)
@@ -113,7 +120,7 @@ class XEP_0363(BasePlugin):
if feature == Request.namespace:
return info
def request_slot(self, jid: JID, filename: str, size: int,
def request_slot(self, jid: JID, filename: Path, size: int,
content_type: Optional[str] = None, *,
ifrom: Optional[JID] = None, **iqkwargs) -> Future:
"""Request an HTTP upload slot from a service.
@@ -125,12 +132,12 @@ class XEP_0363(BasePlugin):
"""
iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom)
request = iq['http_upload_request']
request['filename'] = filename
request['filename'] = str(filename)
request['size'] = str(size)
request['content-type'] = content_type or self.default_content_type
return iq.send(**iqkwargs)
async def upload_file(self, filename: str, size: Optional[int] = None,
async def upload_file(self, filename: Path, size: Optional[int] = None,
content_type: Optional[str] = None, *,
input_file: Optional[IO[bytes]]=None,
domain: Optional[JID] = None,

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

@@ -6,9 +6,7 @@
from typing import Set, Iterable
from slixmpp.xmlstream import ElementBase
try:
from emoji import UNICODE_EMOJI
if UNICODE_EMOJI.get('en'):
UNICODE_EMOJI = UNICODE_EMOJI['en']
from emoji import EMOJI_DATA as UNICODE_EMOJI
except ImportError:
UNICODE_EMOJI = None

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

@@ -0,0 +1,176 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fenc=utf-8 et ts=4 sts=4 sw=4
#
# Copyright © 2022 Maxime “pep” Buquet <pep@bouah.net>
#
# See the LICENSE file for copying permissions.
"""
XEP-0454: OMEMO Media Sharing
"""
from typing import IO, Optional, Tuple
from os import urandom
from pathlib import Path
from io import BytesIO, SEEK_END
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.base import register_plugin
class InvalidURL(Exception):
"""Raised for URLs that either aren't HTTPS or already contain a fragment."""
EXTENSIONS_MAP = {
'jpeg': 'jpg',
'text': 'txt',
}
class XEP_0454(BasePlugin):
"""
XEP-0454: OMEMO Media Sharing
"""
name = 'xep_0454'
description = 'XEP-0454: OMEMO Media Sharing'
dependencies = {'xep_0363'}
@staticmethod
def encrypt(input_file: Optional[IO[bytes]] = None, filename: Optional[Path] = None) -> Tuple[bytes, str]:
"""
Encrypts file as specified in XEP-0454 for use in file sharing
:param input_file: Binary file stream on the file.
:param filename: Path to the file to upload.
One of input_file or filename must be specified. If both are
passed, input_file will be used and filename ignored.
"""
if input_file is None and filename is None:
raise ValueError('Specify either filename or input_file parameter')
aes_gcm_iv = urandom(12)
aes_gcm_key = urandom(32)
aes_gcm = Cipher(
algorithms.AES(aes_gcm_key),
modes.GCM(aes_gcm_iv),
).encryptor()
if input_file is None:
input_file = open(filename, 'rb')
payload = b''
while True:
buf = input_file.read(4096)
if not buf:
break
payload += aes_gcm.update(buf)
payload += aes_gcm.finalize() + aes_gcm.tag
fragment = aes_gcm_iv.hex() + aes_gcm_key.hex()
return (payload, fragment)
@staticmethod
def decrypt(input_file: IO[bytes], fragment: str) -> bytes:
"""
Decrypts file-like.
:param input_file: Binary file stream on the file, containing the
tag (16 bytes) at the end.
:param fragment: 88 hex chars string composed of iv (24 chars)
+ key (64 chars).
"""
assert len(fragment) == 88
aes_gcm_iv = bytes.fromhex(fragment[:24])
aes_gcm_key = bytes.fromhex(fragment[24:])
# Find 16 bytes tag
input_file.seek(-16, SEEK_END)
tag = input_file.read()
aes_gcm = Cipher(
algorithms.AES(aes_gcm_key),
modes.GCM(aes_gcm_iv, tag),
).decryptor()
size = input_file.seek(0, SEEK_END)
input_file.seek(0)
count = size - 16
plain = b''
while count >= 0:
buf = input_file.read(4096)
count -= len(buf)
if count <= 0:
buf += input_file.read()
buf = buf[:-16]
plain += aes_gcm.update(buf)
plain += aes_gcm.finalize()
return plain
@staticmethod
def format_url(url: str, fragment: str) -> str:
"""Helper to format a HTTPS URL to an AESGCM URI"""
if not url.startswith('https://') or url.find('#') != -1:
raise InvalidURL
return 'aesgcm://' + url[len('https://'):] + '#' + fragment
@staticmethod
def map_extensions(ext: str) -> str:
"""
Apply conversions to extensions to reduce the number of
variations, (e.g., JPEG -> jpg).
"""
return EXTENSIONS_MAP.get(ext, ext).lower()
async def upload_file(
self,
filename: Path,
_size: Optional[int] = None,
content_type: Optional[str] = None,
**kwargs,
) -> str:
"""
Wrapper to xep_0363 (HTTP Upload)'s upload_file method.
:param input_file: Binary file stream on the file.
:param filename: Path to the file to upload.
Same as `XEP_0454.encrypt`, one of input_file or filename must be
specified. If both are passed, input_file will be used and
filename ignored.
Other arguments passed in are passed to the actual
`XEP_0363.upload_file` call.
"""
input_file = kwargs.get('input_file')
payload, fragment = self.encrypt(input_file, filename)
# Prepare kwargs for upload_file call
new_filename = urandom(12).hex() # Random filename to hide user-provided path
if filename.suffix:
new_filename += self.map_extensions(filename.suffix)
kwargs['filename'] = new_filename
input_enc = BytesIO(payload)
kwargs['input_file'] = input_enc
# Size must also be overriden if provided
size = input_enc.seek(0, SEEK_END)
input_enc.seek(0)
kwargs['size'] = size
kwargs['content_type'] = content_type
url = await self.xmpp['xep_0363'].upload_file(**kwargs)
return self.format_url(url, fragment)
register_plugin(XEP_0454)

View File

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

View File

@@ -0,0 +1,48 @@
from slixmpp.plugins import BasePlugin
from slixmpp.types import JidStr
from slixmpp.xmlstream import StanzaBase
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from . import stanza
class XEP_0461(BasePlugin):
"""XEP-0461: Message Replies"""
name = "xep_0461"
description = "XEP-0461: Message Replies"
dependencies = {"xep_0030"}
stanza = stanza
namespace = stanza.NS
def plugin_init(self) -> None:
stanza.register_plugins()
self.xmpp.register_handler(
Callback(
"Message replied to",
StanzaPath("message/reply"),
self._handle_reply_to_message,
)
)
def plugin_end(self):
self.xmpp.plugin["xep_0030"].del_feature(feature=stanza.NS)
def session_bind(self, jid):
self.xmpp.plugin["xep_0030"].add_feature(feature=stanza.NS)
def _handle_reply_to_message(self, msg: StanzaBase):
self.xmpp.event("message_reply", msg)
def send_reply(self, reply_to: JidStr, reply_id: str, **msg_kwargs):
"""
:param reply_to: Full JID of the quoted author
:param reply_id: ID of the message to reply to
"""
msg = self.xmpp.make_message(**msg_kwargs)
msg["reply"]["to"] = reply_to
msg["reply"]["id"] = reply_id
msg.send()

View File

@@ -0,0 +1,95 @@
from typing import Optional
from slixmpp.stanza import Message
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
NS = "urn:xmpp:reply:0"
class Reply(ElementBase):
namespace = NS
name = "reply"
plugin_attrib = "reply"
interfaces = {"id", "to"}
class FeatureFallBack(ElementBase):
# should also be a multi attrib
namespace = "urn:xmpp:fallback:0"
name = "fallback"
plugin_attrib = "feature_fallback"
interfaces = {"for"}
def get_fallback_body(self):
# only works for a single fallback_body attrib
start = self["fallback_body"]["start"]
end = self["fallback_body"]["end"]
body = self.parent()["body"]
if start <= end:
return body[start:end]
else:
return ""
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"]
if start <= end < len(body):
return body[:start] + body[end:]
else:
return body
def add_quoted_fallback(self, fallback: str, nickname: Optional[str] = None):
"""
Add plain text fallback for clients not implementing XEP-0461.
``msg["feature_fallback"].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.
: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"]
msg["feature_fallback"]["for"] = NS
msg["feature_fallback"]["fallback_body"]["start"] = 0
msg["feature_fallback"]["fallback_body"]["end"] = len(quoted)
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"}
def set_start(self, v: int):
self._set_attr("start", str(v))
def get_start(self):
try:
return int(self._get_attr("start"))
except ValueError:
return 0
def set_end(self, v: int):
self._set_attr("end", str(v))
def get_end(self):
try:
return int(self._get_attr("end"))
except ValueError:
return 0
def register_plugins():
register_stanza_plugin(Message, Reply)
register_stanza_plugin(Message, FeatureFallBack)
register_stanza_plugin(FeatureFallBack, FallBackBody)

View File

@@ -64,9 +64,9 @@ class Message(RootStanza):
if self.stream:
use_ids = getattr(self.stream, 'use_message_ids', None)
if use_ids:
self['id'] = self.stream.new_id()
self.set_id(self.stream.new_id())
else:
del self['origin_id']
self.del_origin_id()
def get_type(self):
"""
@@ -96,8 +96,8 @@ class Message(RootStanza):
self.xml.attrib['id'] = value
if self.stream:
use_orig_ids = getattr(self.stream, 'use_origin_id', None)
if not use_orig_ids:
if not getattr(self.stream, 'use_origin_id', False):
self.del_origin_id()
return None
sub = self.xml.find(ORIGIN_NAME)
@@ -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

@@ -10,11 +10,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 +324,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 +743,10 @@ 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)

View File

@@ -3,5 +3,4 @@ try:
except:
from slixmpp.thirdparty.gnupg import GPG
from slixmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso
from slixmpp.thirdparty.orderedset import OrderedSet

View File

@@ -1,273 +0,0 @@
# This module is a very stripped down version of the dateutil
# package for when dateutil has not been installed. As a replacement
# for dateutil.parser.parse, the parsing methods from
# http://blog.mfabrik.com/2008/06/30/relativity-of-time-shortcomings-in-python-datetime-and-workaround/
#As such, the following copyrights and licenses applies:
# dateutil - Extensions to the standard python 2.3+ datetime module.
#
# Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# fixed_dateime
#
# Copyright (c) 2008, Red Innovation Ltd., Finland
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of Red Innovation nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import re
import math
import datetime
ZERO = datetime.timedelta(0)
try:
from dateutil.parser import parse as parse_iso
from dateutil.tz import tzoffset, tzutc
except:
# As a stopgap, define the two timezones here based
# on the dateutil code.
class tzutc(datetime.tzinfo):
def utcoffset(self, dt):
return ZERO
def dst(self, dt):
return ZERO
def tzname(self, dt):
return "UTC"
def __eq__(self, other):
return (isinstance(other, tzutc) or
(isinstance(other, tzoffset) and other._offset == ZERO))
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "%s()" % self.__class__.__name__
__reduce__ = object.__reduce__
class tzoffset(datetime.tzinfo):
def __init__(self, name, offset):
self._name = name
self._offset = datetime.timedelta(minutes=offset)
def utcoffset(self, dt):
return self._offset
def dst(self, dt):
return ZERO
def tzname(self, dt):
return self._name
def __eq__(self, other):
return (isinstance(other, tzoffset) and
self._offset == other._offset)
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "%s(%s, %s)" % (self.__class__.__name__,
repr(self._name),
self._offset.days*86400+self._offset.seconds)
__reduce__ = object.__reduce__
_fixed_offset_tzs = { }
UTC = tzutc()
def _get_fixed_offset_tz(offsetmins):
"""For internal use only: Returns a tzinfo with
the given fixed offset. This creates only one instance
for each offset; the zones are kept in a dictionary"""
if offsetmins == 0:
return UTC
if not offsetmins in _fixed_offset_tzs:
if offsetmins < 0:
sign = '-'
absoff = -offsetmins
else:
sign = '+'
absoff = offsetmins
name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60)
inst = tzoffset(name,offsetmins)
_fixed_offset_tzs[offsetmins] = inst
return _fixed_offset_tzs[offsetmins]
_iso8601_parser = re.compile(r"""
^
(?P<year> [0-9]{4})?(?P<ymdsep>-?)?
(?P<month>[0-9]{2})?(?P=ymdsep)?
(?P<day> [0-9]{2})?
(?P<time>
(?: # time part... optional... at least hour must be specified
(?:T|\s+)?
(?P<hour>[0-9]{2})
(?:
# minutes, separated with :, or none, from hours
(?P<hmssep>[:]?)
(?P<minute>[0-9]{2})
(?:
# same for seconds, separated with :, or none, from hours
(?P=hmssep)
(?P<second>[0-9]{2})
)?
)?
# fractions
(?: [,.] (?P<frac>[0-9]{1,10}))?
# timezone, Z, +-hh or +-hh:?mm. MUST BE, but complain if not there.
(
(?P<tzempty>Z)
|
(?P<tzh>[+-][0-9]{2})
(?: :? # optional separator
(?P<tzm>[0-9]{2})
)?
)?
)
)?
$
""", re.X) # """
def parse_iso(timestamp):
"""Internal function for parsing a timestamp in
ISO 8601 format"""
timestamp = timestamp.strip()
m = _iso8601_parser.match(timestamp)
if not m:
raise ValueError("Not a proper ISO 8601 timestamp!: %s" % timestamp)
vals = m.groupdict()
def_vals = {'year': 1970, 'month': 1, 'day': 1}
for key in vals:
if vals[key] is None:
vals[key] = def_vals.get(key, 0)
elif key not in ['time', 'ymdsep', 'hmssep', 'tzempty']:
vals[key] = int(vals[key])
year = vals['year']
month = vals['month']
day = vals['day']
if m.group('time') is None:
return datetime.date(year, month, day)
h, min, s, us = None, None, None, 0
frac = 0
if m.group('tzempty') == None and m.group('tzh') == None:
raise ValueError("Not a proper ISO 8601 timestamp: " +
"missing timezone (Z or +hh[:mm])!")
if m.group('frac'):
frac = m.group('frac')
power = len(frac)
frac = int(frac) / 10.0 ** power
if m.group('hour'):
h = vals['hour']
if m.group('minute'):
min = vals['minute']
if m.group('second'):
s = vals['second']
if frac != None:
# ok, fractions of hour?
if min == None:
frac, min = math.modf(frac * 60.0)
min = int(min)
# fractions of second?
if s == None:
frac, s = math.modf(frac * 60.0)
s = int(s)
# and extract microseconds...
us = int(frac * 1000000)
if m.group('tzempty') == 'Z':
offsetmins = 0
else:
# timezone: hour diff with sign
offsetmins = vals['tzh'] * 60
tzm = m.group('tzm')
# add optional minutes
if tzm != None:
tzm = int(tzm)
offsetmins += tzm if offsetmins > 0 else -tzm
tz = _get_fixed_offset_tz(offsetmins)
return datetime.datetime(year, month, day, h, min, s, us, tz)

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

@@ -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.1'
__version_info__ = (1, 8, 1)
__version__ = '1.8.4'
__version_info__ = (1, 8, 4)

View File

@@ -15,7 +15,13 @@ from slixmpp.types import Protocol
log = logging.getLogger(__name__)
class AnswerProtocol(Protocol):
class GetHostByNameAnswerProtocol(Protocol):
name: str
aliases: List[str]
addresses: List[str]
class QueryAnswerProtocol(Protocol):
host: str
priority: int
weight: int
@@ -23,6 +29,9 @@ class AnswerProtocol(Protocol):
class ResolverProtocol(Protocol):
def gethostbyname(self, host: str, socket_family: socket.AddressFamily) -> Future:
...
def query(self, query: str, querytype: str) -> Future:
...
@@ -147,11 +156,6 @@ async def resolve(host: str, port: int, *, loop: AbstractEventLoop,
results = []
for host, port in hosts:
if host == 'localhost':
if use_ipv6:
results.append((host, '::1', port))
results.append((host, '127.0.0.1', port))
if use_ipv6:
aaaa = await get_AAAA(host, resolver=resolver,
use_aiodns=use_aiodns, loop=loop)
@@ -201,13 +205,13 @@ async def get_A(host: str, *, loop: AbstractEventLoop,
return []
# Using aiodns:
future = resolver.query(host, 'A')
future = resolver.gethostbyname(host, socket.AF_INET)
try:
recs = cast(Iterable[AnswerProtocol], await future)
recs = cast(GetHostByNameAnswerProtocol, await future)
except Exception as e:
log.debug('DNS: Exception while querying for %s A records: %s', host, e)
recs = []
return [rec.host for rec in recs]
return []
return [addr for addr in recs.addresses]
async def get_AAAA(host: str, *, loop: AbstractEventLoop,
@@ -249,13 +253,13 @@ async def get_AAAA(host: str, *, loop: AbstractEventLoop,
return []
# Using aiodns:
future = resolver.query(host, 'AAAA')
future = resolver.gethostbyname(host, socket.AF_INET6)
try:
recs = cast(Iterable[AnswerProtocol], await future)
recs = cast(GetHostByNameAnswerProtocol, await future)
except Exception as e:
log.debug('DNS: Exception while querying for %s AAAA records: %s', host, e)
recs = []
return [rec.host for rec in recs]
return []
return [addr for addr in recs.addresses]
async def get_SRV(host: str, port: int, service: str,
@@ -295,12 +299,12 @@ async def get_SRV(host: str, port: int, service: str,
try:
future = resolver.query('_%s._%s.%s' % (service, proto, host),
'SRV')
recs = cast(Iterable[AnswerProtocol], await future)
recs = cast(Iterable[QueryAnswerProtocol], await future)
except Exception as e:
log.debug('DNS: Exception while querying for %s SRV records: %s', host, e)
return []
answers: Dict[int, List[AnswerProtocol]] = {}
answers: Dict[int, List[QueryAnswerProtocol]] = {}
for rec in recs:
if rec.priority not in answers:
answers[rec.priority] = []

View File

@@ -35,6 +35,7 @@ import ssl
import uuid
import warnings
import weakref
import collections
from contextlib import contextmanager
import xml.etree.ElementTree as ET
@@ -82,7 +83,7 @@ class InvalidCABundle(Exception):
Exception raised when the CA Bundle file hasn't been found.
"""
def __init__(self, path: Optional[Path]):
def __init__(self, path: Optional[Union[Path, Iterable[Path]]]):
self.path = path
@@ -298,8 +299,8 @@ class XMLStream(asyncio.BaseProtocol):
self.scheduled_events = {}
self.ssl_context = ssl.create_default_context()
self.ssl_context.check_hostname = False
self.ssl_context.verify_mode = ssl.CERT_NONE
self.ssl_context.check_hostname = True
self.ssl_context.verify_mode = ssl.CERT_REQUIRED
self.event_when_connected = "connected"
@@ -483,25 +484,21 @@ class XMLStream(asyncio.BaseProtocol):
if self._current_connection_attempt is None:
return
try:
server_hostname = self.default_domain if self.use_ssl else None
await self.loop.create_connection(lambda: self,
self.address[0],
self.address[1],
ssl=ssl_context,
server_hostname=self.default_domain if self.use_ssl else None)
server_hostname=server_hostname)
self._connect_loop_wait = 0
except Socket.gaierror as e:
self.event('connection_failed',
'No DNS record available for %s' % self.default_domain)
self.reschedule_connection_attempt()
except OSError as e:
log.debug('Connection failed: %s', e)
self.event("connection_failed", e)
if self._current_connection_attempt is None:
return
self._connect_loop_wait = self._connect_loop_wait * 2 + 1
self._current_connection_attempt = asyncio.ensure_future(
self._connect_routine(),
loop=self.loop,
)
self.reschedule_connection_attempt()
def process(self, *, forever: bool = True, timeout: Optional[int] = None) -> None:
"""Process all the available XMPP events (receiving or sending data on the
@@ -578,7 +575,7 @@ class XMLStream(asyncio.BaseProtocol):
stream=self,
top_level=True,
open_only=True))
self.start_stream_handler(self.xml_root)
self.start_stream_handler(self.xml_root) # type:ignore
self.xml_depth += 1
if event == 'end':
self.xml_depth -= 1
@@ -637,6 +634,20 @@ class XMLStream(asyncio.BaseProtocol):
self._set_disconnected_future()
self.event("disconnected", self.disconnect_reason or exception)
def reschedule_connection_attempt(self) -> None:
"""
Increase the exponential back-off and initate another background
_connect_routine call to connect to the server.
"""
# abort if there is no ongoing connection attempt
if self._current_connection_attempt is None:
return
self._connect_loop_wait = min(300, self._connect_loop_wait * 2 + 1)
self._current_connection_attempt = asyncio.ensure_future(
self._connect_routine(),
loop=self.loop,
)
def cancel_connection_attempt(self) -> None:
"""
Immediately cancel the current create_connection() Future.
@@ -793,11 +804,14 @@ class XMLStream(asyncio.BaseProtocol):
if bundle.is_file():
ca_cert = bundle
break
if ca_cert is None:
raise InvalidCABundle(ca_cert)
if ca_cert is None and \
isinstance(self.ca_certs, (Path, collections.abc.Iterable)):
raise InvalidCABundle(self.ca_certs)
self.ssl_context.verify_mode = ssl.CERT_REQUIRED
self.ssl_context.load_verify_locations(cafile=ca_cert)
else:
self.ssl_context.set_default_verify_paths()
return self.ssl_context
@@ -814,15 +828,15 @@ class XMLStream(asyncio.BaseProtocol):
try:
if hasattr(self.loop, 'start_tls'):
transp = await self.loop.start_tls(self.transport,
self, ssl_context)
self, ssl_context,
server_hostname=self.default_domain)
# Python < 3.7
else:
transp, _ = await self.loop.create_connection(
lambda: self,
ssl=self.ssl_context,
sock=self.socket,
server_hostname=self.default_domain
)
server_hostname=self.default_domain)
except ssl.SSLError as e:
log.debug('SSL: Unable to connect', exc_info=True)
log.error('CERT: Invalid certificate trust chain.')
@@ -1254,7 +1268,7 @@ class XMLStream(asyncio.BaseProtocol):
already_run_filters.add(filter)
if iscoroutinefunction(filter):
filter = cast(AsyncFilter, filter)
task = asyncio.create_task(filter(data))
task = asyncio.create_task(filter(data)) # type:ignore
completed, pending = await wait(
{task},
timeout=1,
@@ -1318,10 +1332,18 @@ class XMLStream(asyncio.BaseProtocol):
# Avoid circular imports
from slixmpp.stanza.rootstanza import RootStanza
from slixmpp.stanza import Iq, Handshake
passthrough = (
(isinstance(data, Iq) and data.get_plugin('bind', check=True))
or isinstance(data, Handshake)
)
passthrough = False
if isinstance(data, Iq):
if data.get_plugin('bind', check=True):
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
if isinstance(data, (RootStanza, str)) and not passthrough:
self.__queued_stanzas.append((data, use_filters))
log.debug('NOT SENT: %s %s', type(data), data)

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

@@ -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,59 @@
import unittest
from slixmpp import register_stanza_plugin, Iq
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0055 import stanza
class TestJabberSearch(SlixTest):
def setUp(self):
register_stanza_plugin(Iq, stanza.Search)
self.stream_start(plugins={"xep_0055"})
def testRequestSearchFields(self):
iq = self.Iq()
iq.set_from("juliet@capulet.com/balcony")
iq.set_to("characters.shakespeare.lit")
iq.set_type("get")
iq.enable("search")
iq["id"] = "0"
self.check(
iq,
"""
<iq type='get'
from='juliet@capulet.com/balcony'
to='characters.shakespeare.lit'>
<query xmlns='jabber:iq:search'/>
</iq>
""",
)
def testSendSearch(self):
iq = self.xmpp["xep_0055"].make_search_iq(
ifrom="juliet@capulet.com/balcony", ito="characters.shakespeare.lit"
)
iq["search"]["form"].add_field(var="x-gender", value="male")
self.check(
iq,
"""
<iq type='set'
from='juliet@capulet.com/balcony'
to='characters.shakespeare.lit'>
<query xmlns='jabber:iq:search'>
<x xmlns='jabber:x:data' type='submit'>
<field type='hidden' var='FORM_TYPE'>
<value>jabber:iq:search</value>
</field>
<field var='x-gender'>
<value>male</value>
</field>
</x>
</query>
</iq>
""",
use_values=False,
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberSearch)

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

@@ -13,7 +13,7 @@ class TestPermissions(SlixTest):
def testAdvertisePermission(self):
xmlstring = """
<message from='capulet.net' to='pubub.capulet.lit'>
<privilege xmlns='urn:xmpp:privilege:1'>
<privilege xmlns='urn:xmpp:privilege:2'>
<perm access='roster' type='both'/>
<perm access='message' type='outgoing'/>
<perm access='presence' type='managed_entity'/>

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,75 @@
import unittest
from slixmpp import Message
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0461 import stanza
class TestReply(SlixTest):
def setUp(self):
stanza.register_plugins()
def testReply(self):
message = Message()
message["reply"]["id"] = "some-id"
message["body"] = "some-body"
self.check(
message,
"""
<message>
<reply xmlns="urn:xmpp:reply:0" id="some-id" />
<body>some-body</body>
</message>
""",
)
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
self.check(
message,
"""
<message xmlns="jabber:client">
<body>12345\nrealbody</body>
<fallback xmlns='urn:xmpp:fallback:0' for='NS'>
<body start="0" end="6" />
</fallback>
</message>
""",
)
assert message["feature_fallback"].get_stripped_body() == "realbody"
def testAddFallBackHelper(self):
msg = Message()
msg["body"] = "Great"
msg["feature_fallback"].add_quoted_fallback("Anna wrote:\nHi, how are you?")
# ugly dedent but the test does not pass without it
self.check(
msg,
"""
<message xmlns="jabber:client" type="normal">
<body>> Anna wrote:\n> Hi, how are you?\nGreat</body>
<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["feature_fallback"].add_quoted_fallback(body)
body2 = msg["feature_fallback"].get_fallback_body()
self.assertTrue(body2 == quoted, body2)
suite = unittest.TestLoader().loadTestsFromTestCase(TestReply)

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."""

View File

@@ -11,9 +11,6 @@ class TestStreamPresence(SlixTest):
def setUp(self):
self.stream_start(jid='tester@localhost', plugins=[])
def tearDown(self):
self.stream_close()
def testInitialUnavailablePresences(self):
"""
Test receiving unavailable presences from JIDs that

View File

@@ -13,9 +13,6 @@ class TestStreamRoster(SlixTest):
Test handling roster updates.
"""
def tearDown(self):
self.stream_close()
def testGetRoster(self):
"""Test handling roster requests."""
self.stream_start(mode='client', jid='tester@localhost')

View File

@@ -11,9 +11,6 @@ class TestStreamDisco(SlixTest):
Test using the XEP-0030 plugin.
"""
def tearDown(self):
self.stream_close()
def testInfoEmptyDefaultNode(self):
"""
Info query result from an entity MUST have at least one identity

View File

@@ -11,9 +11,6 @@ class TestInBandByteStreams(SlixTest):
def setUp(self):
self.stream_start(plugins=['xep_0047', 'xep_0030'])
def tearDown(self):
self.stream_close()
def testOpenStream(self):
"""Test requesting a stream, successfully"""

View File

@@ -16,9 +16,6 @@ class TestAdHocCommands(SlixTest):
# a dummy value.
self.xmpp['xep_0050'].new_session = lambda: '_sessionid_'
def tearDown(self):
self.stream_close()
def testInitialPayloadCommand(self):
"""Test a command with an initial payload."""

View File

@@ -0,0 +1,167 @@
import unittest
from slixmpp.test import SlixTest
class TestJabberSearch(SlixTest):
def setUp(self):
self.stream_start(
mode="component",
plugin_config={
"xep_0055": {
"form_fields": {"first", "last"},
"form_instructions": "INSTRUCTIONS",
"form_title": "User Directory Search",
}
},
jid="characters.shakespeare.lit",
plugins={"xep_0055"}
)
self.xmpp["xep_0055"].api.register(get_results, "search_query")
self.xmpp["xep_0055"].api.register(get_results, "search_query")
def testRequestingSearchFields(self):
self.recv(
"""
<iq type='get'
from='juliet@capulet.com/balcony'
to='characters.shakespeare.lit'
id='search3'
xml:lang='en'>
<query xmlns='jabber:iq:search'/>
</iq>
"""
)
self.send(
"""
<iq type='result'
from='characters.shakespeare.lit'
to='juliet@capulet.com/balcony'
id='search3'
xml:lang='en'>
<query xmlns='jabber:iq:search'>
<x xmlns='jabber:x:data' type='form'>
<title>User Directory Search</title>
<instructions>INSTRUCTIONS</instructions>
<field type='hidden'
var='FORM_TYPE'>
<value>jabber:iq:search</value>
</field>
<field var='first'/>
<field var='last'/>
</x>
</query>
</iq>
""",
use_values=False,
)
def testSearchResult(self):
self.recv(
"""
<iq type='get'
from='juliet@capulet.com/balcony'
to='characters.shakespeare.lit'
id='search2'
xml:lang='en'>
<query xmlns='jabber:iq:search'>
<x xmlns='jabber:x:data' type='submit'>
<field type='hidden' var='FORM_TYPE'>
<value>jabber:iq:search</value>
</field>
<field var='last'>
<value>Montague</value>
</field>
</x>
</query>
</iq>
"""
)
self.send(
"""
<iq type='result'
from='characters.shakespeare.lit'
to='juliet@capulet.com/balcony'
id='search2'
xml:lang='en'>
<query xmlns='jabber:iq:search'>
<x xmlns='jabber:x:data' type='result'>
<field type='hidden' var='FORM_TYPE'>
<value>jabber:iq:search</value>
</field>
<reported>
<field var='first' label='Given Name' />
<field var='last' label='Family Name' />
</reported>
<item>
<field var='first'><value>Benvolio</value></field>
<field var='last'><value>Montague</value></field>
</item>
</x>
</query>
</iq>
""",
use_values=False, # TypeError: element indices must be integers without that
)
def testSearchNoResult(self):
self.xmpp["xep_0055"].api.register(get_results, "search_query")
self.recv(
"""
<iq type='get'
from='juliet@capulet.com/balcony'
to='characters.shakespeare.lit'
id='search2'
xml:lang='en'>
<query xmlns='jabber:iq:search'>
<x xmlns='jabber:x:data' type='submit'>
<field type='hidden' var='FORM_TYPE'>
<value>jabber:iq:search</value>
</field>
<field var='last'>
<value>Capulet</value>
</field>
</x>
</query>
</iq>
"""
)
self.send(
"""
<iq type='result'
from='characters.shakespeare.lit'
to='juliet@capulet.com/balcony'
id='search2'
xml:lang='en'>
<query xmlns='jabber:iq:search'>
<x xmlns='jabber:x:data' type='result'>
<field type='hidden' var='FORM_TYPE'>
<value>jabber:iq:search</value>
</field>
<reported>
<field var='first' label='Given Name' />
<field var='last' label='Family Name' />
</reported>
</x>
</query>
</iq>
""",
use_values=False, # TypeError: element indices must be integers without that
)
async def get_results(jid, node, ifrom, iq):
reply = iq.reply()
form = reply["search"]["form"]
form["type"] = "result"
form.add_reported("first", label="Given Name")
form.add_reported("last", label="Family Name")
d = iq["search"]["form"].get_values()
if d["last"] == "Montague":
form.add_item({"first": "Benvolio", "last": "Montague"})
return reply
suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberSearch)

View File

@@ -15,9 +15,6 @@ class TestStreamPubsub(SlixTest):
def setUp(self):
self.stream_start()
def tearDown(self):
self.stream_close()
def testCreateInstantNode(self):
"""Test creating an instant node"""
self.xmpp['xep_0060'].create_node('pubsub.example.com', None)

View File

@@ -6,9 +6,6 @@ from slixmpp.test import SlixTest
class TestOOB(SlixTest):
def tearDown(self):
self.stream_close()
def testSendOOB(self):
"""Test sending an OOB transfer request."""
self.stream_start(plugins=['xep_0066', 'xep_0030'])

View File

@@ -6,9 +6,6 @@ from slixmpp.test import SlixTest
class TestStreamChatStates(SlixTest):
def tearDown(self):
self.stream_close()
def testChatStates(self):
self.stream_start(mode='client', plugins=['xep_0030', 'xep_0085'])

View File

@@ -6,9 +6,6 @@ from slixmpp.test import SlixTest
class TestStreamSet(SlixTest):
def tearDown(self):
self.stream_close()
def testHandleSoftwareVersionRequest(self):
self.stream_start(mode='client', plugins=['xep_0030', 'xep_0092'])

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