Compare commits

..

36 Commits

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

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

Unfortunately that was broken for some of the parameters and resulted
different TLS related settings on reconnections. This commit fixes that.
2023-12-27 11:45:04 +01:00
31 changed files with 643 additions and 36 deletions

6
.woodpecker/lint.yml Normal file
View File

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

View File

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

17
.woodpecker/test.yml Normal file
View File

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

View File

@@ -1064,5 +1064,12 @@
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.4.tar.gz"/> <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.4.tar.gz"/>
</Version> </Version>
</release> </release>
<release>
<Version>
<revision>1.8.5</revision>
<created>2024-02-02</created>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.5.tar.gz"/>
</Version>
</release>
</Project> </Project>
</rdf:RDF> </rdf:RDF>

95
docs/projects.rst Normal file
View File

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

View File

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

View File

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

View File

@@ -9,6 +9,8 @@
import logging import logging
import hashlib import hashlib
from typing import Optional
from slixmpp import Message, Iq, Presence from slixmpp import Message, Iq, Presence
from slixmpp.basexmpp import BaseXMPP from slixmpp.basexmpp import BaseXMPP
from slixmpp.stanza import Handshake from slixmpp.stanza import Handshake
@@ -93,7 +95,9 @@ class ComponentXMPP(BaseXMPP):
for st in Message, Iq, Presence: for st in Message, Iq, Presence:
register_stanza_plugin(st, Error) register_stanza_plugin(st, Error)
def connect(self, host=None, port=None, use_ssl=False): def connect(self, host: Optional[str] = None, port: int = 0, use_ssl: Optional[bool] = None,
force_starttls: Optional[bool] = None,
disable_starttls: Optional[bool] = None) -> None:
"""Connect to the server. """Connect to the server.
@@ -103,17 +107,18 @@ class ComponentXMPP(BaseXMPP):
Defauts to :attr:`server_port`. Defauts to :attr:`server_port`.
:param use_ssl: Flag indicating if SSL should be used by connecting :param use_ssl: Flag indicating if SSL should be used by connecting
directly to a port using SSL. directly to a port using SSL.
:param force_starttls: UNUSED
:param disable_starttls: UNUSED
""" """
if host is None: if host is not None:
host = self.server_host self.server_host = host
if port is None: if port:
port = self.server_port self.server_port = port
self.server_name = self.boundjid.host self.server_name = self.boundjid.host
log.debug("Connecting to %s:%s", host, port) log.debug("Connecting to %s:%s", host, port)
return XMLStream.connect(self, host=host, port=port, XMLStream.connect(self, host=self.server_host, port=self.server_port, use_ssl=use_ssl)
use_ssl=use_ssl)
def incoming_filter(self, xml): def incoming_filter(self, xml):
""" """

View File

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

View File

@@ -76,6 +76,7 @@ PLUGINS = [
'xep_0256', # Last Activity in Presence 'xep_0256', # Last Activity in Presence
'xep_0257', # Client Certificate Management for SASL EXTERNAL 'xep_0257', # Client Certificate Management for SASL EXTERNAL
'xep_0258', # Security Labels in XMPP 'xep_0258', # Security Labels in XMPP
'xep_0264', # Jingle Content Thumbnails
# 'xep_0270', # XMPP Compliance Suites 2010. Dont automatically load # 'xep_0270', # XMPP Compliance Suites 2010. Dont automatically load
'xep_0279', # Server IP Check 'xep_0279', # Server IP Check
'xep_0280', # Message Carbons 'xep_0280', # Message Carbons
@@ -85,6 +86,7 @@ PLUGINS = [
# 'xep_0302', # XMPP Compliance Suites 2012. Dont automatically load # 'xep_0302', # XMPP Compliance Suites 2012. Dont automatically load
'xep_0308', # Last Message Correction 'xep_0308', # Last Message Correction
'xep_0313', # Message Archive Management 'xep_0313', # Message Archive Management
'xep_0317', # Hats
'xep_0319', # Last User Interaction in Presence 'xep_0319', # Last User Interaction in Presence
# 'xep_0323', # IoT Systems Sensor Data. Dont automatically load # 'xep_0323', # IoT Systems Sensor Data. Dont automatically load
# 'xep_0325', # IoT Systems Control. Dont automatically load # 'xep_0325', # IoT Systems Control. Dont automatically load
@@ -118,6 +120,7 @@ PLUGINS = [
'xep_0444', # Message Reactions 'xep_0444', # Message Reactions
'xep_0447', # Stateless file sharing 'xep_0447', # Stateless file sharing
'xep_0461', # Message Replies 'xep_0461', # Message Replies
'xep_0469', # Bookmarks Pinning
# Meant to be imported by plugins # Meant to be imported by plugins
] ]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
# Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
# This file is part of Slixmpp. # This file is part of Slixmpp.
# See the file LICENSE for copying permission. # See the file LICENSE for copying permission.
import atexit
import unittest import unittest
from queue import Queue from queue import Queue
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
@@ -750,3 +751,12 @@ class SlixTest(unittest.TestCase):
Error.namespace = 'jabber:client' Error.namespace = 'jabber:client'
for st in Message, Iq, Presence: for st in Message, Iq, Presence:
register_stanza_plugin(st, Error) register_stanza_plugin(st, Error)
@atexit.register
def cleanup():
try:
loop = asyncio.get_event_loop()
loop.close()
except:
pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,76 @@
import logging
import unittest
from slixmpp.test import SlixTest
class TestCaps(SlixTest):
def setUp(self):
self.stream_start(plugins=["xep_0115"])
def testConcurrentSameHash(self):
"""
Check that we only resolve a given ver string to a disco info once,
even if we receive several presences with that same ver string
consecutively.
"""
self.recv( # language=XML
"""
<presence from='romeo@montague.lit/orchard'>
<c xmlns='http://jabber.org/protocol/caps'
hash='sha-1'
node='a-node'
ver='h0TdMvqNR8FHUfFG1HauOLYZDqE='/>
</presence>
"""
)
self.recv( # language=XML
"""
<presence from='i-dont-know-much-shakespeare@montague.lit/orchard'>
<c xmlns='http://jabber.org/protocol/caps'
hash='sha-1'
node='a-node'
ver='h0TdMvqNR8FHUfFG1HauOLYZDqE='/>
</presence>
"""
)
self.send( # language=XML
"""
<iq xmlns="jabber:client"
id="1"
to="romeo@montague.lit/orchard"
type="get">
<query xmlns="http://jabber.org/protocol/disco#info"
node="a-node#h0TdMvqNR8FHUfFG1HauOLYZDqE="/>
</iq>
"""
)
self.send(None)
self.recv( # language=XML
"""
<iq from='romeo@montague.lit/orchard'
id='1'
type='result'>
<query xmlns='http://jabber.org/protocol/disco#info'
node='a-nodes#h0TdMvqNR8FHUfFG1HauOLYZDqE='>
<identity category='client' name='a client' type='pc'/>
<feature var='http://jabber.org/protocol/caps'/>
</query>
</iq>
"""
)
self.send(None)
self.assertTrue(
self.xmpp["xep_0030"].supports(
"romeo@montague.lit/orchard", "http://jabber.org/protocol/caps"
)
)
self.assertTrue(
self.xmpp["xep_0030"].supports(
"i-dont-know-much-shakespeare@montague.lit/orchard",
"http://jabber.org/protocol/caps",
)
)
logging.basicConfig(level=logging.DEBUG)
suite = unittest.TestLoader().loadTestsFromTestCase(TestCaps)