Compare commits

..

17 Commits

Author SHA1 Message Date
mathieui
a30f76892b
XEP-0482: add initial support 2025-02-09 18:52:28 +01:00
mathieui
3de8ee97b5
XEP-0050: make prev action possible when there is no next action (fixes #3516)
Obviously the session has to allow for it, which must be modified in a
handler.
2025-02-09 16:14:08 +01:00
mathieui
0de9df92c4
xmlstream: do not use the category param to catch_warnings
Added in debian 3.11.
2025-02-09 15:52:43 +01:00
mathieui
04d5c43853
xmlstream/client/componentxmpp: Make mypy happy again
borked in the previous commit regarding connect().
2025-02-09 15:35:40 +01:00
mathieui
0707786057
xmlstream: "cleanl" create a new event loop if none is set
Relates to #3542
2025-02-09 13:39:57 +01:00
mathieui
1c762c6b25
doc: add more info for XEP-0030 (fix #3433) 2025-02-09 12:31:15 +01:00
mathieui
f94a4f2dbd
xmlstream: return a future on connect()
which can make sense for users of the lib to wait on.
2025-02-09 12:12:07 +01:00
mathieui
75ea0bf039 XEP-0308: add tests 2025-02-08 19:51:02 +00:00
mathieui
4cf1286332 XEP-0308: add utility functions
to build and correct messages without needing to go into the xml schema
details.
2025-02-08 19:51:02 +00:00
mathieui
8a127f61d0
XEP-0223: fix node standalone configuration (fixes #3555)
also add a stream test for that
2025-02-08 12:52:33 +01:00
mathieui
1f14fb54c2
XEP-0060: fix get_item_ids (fix #3548)
missing return statement, the function would work with callbacks, but
that is a bit meh.
2025-02-08 12:30:02 +01:00
mathieui
651e0ea593
docs: improve using_asyncio page (hopefully fixes #3562)
make event loop usage a bit clearer, and fix the examples.
2025-02-08 12:26:17 +01:00
mathieui
4ac41a5250
Add a way to get identities as dict (fixes #3566) 2025-02-08 12:11:07 +01:00
mathieui
e03b7661c1 XEP-0446: complete support and tests 2025-02-07 20:33:22 +00:00
DinoThor
e955cd308a Fix bad reference with client & method call 2025-02-07 13:52:15 +00:00
mathieui
2db5e0199c
docs: add lots of missing xeps, fix some issues
sphinx was unhappy with some formatting artifacts
2025-02-03 00:16:51 +01:00
mathieui
bf2e006f88
docs: fix bad targets in projects page
and actually the slixmpp chat room is not a chatroom for random bots
written with slixmpp
2025-02-02 23:43:00 +01:00
38 changed files with 745 additions and 63 deletions

View File

@ -941,6 +941,14 @@
<xmpp:since>1.8.6</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0482.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
<xmpp:since>1.8.7</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0490.html"/>

View File

@ -17,6 +17,7 @@ Plugin index
xep_0049
xep_0050
xep_0054
xep_0055
xep_0059
xep_0060
xep_0065
@ -31,6 +32,7 @@ Plugin index
xep_0085
xep_0086
xep_0092
xep_0100
xep_0106
xep_0107
xep_0108
@ -62,12 +64,15 @@ Plugin index
xep_0256
xep_0257
xep_0258
xep_0264
xep_0279
xep_0280
xep_0292
xep_0297
xep_0300
xep_0308
xep_0313
xep_0317
xep_0319
xep_0332
xep_0333
@ -79,9 +84,13 @@ Plugin index
xep_0359
xep_0363
xep_0369
xep_0372
xep_0377
xep_0380
xep_0382
xep_0385
xep_0394
xep_0402
xep_0403
xep_0404
xep_0405
@ -94,4 +103,9 @@ Plugin index
xep_0439
xep_0441
xep_0444
xep_0446
xep_0447
xep_0461
xep_0469
xep_0490
xep_0492

View File

@ -1,5 +1,5 @@
XEP-0106: Gateway interaction
XEP-0100: Gateway interaction
=============================
.. module:: slixmpp.plugins.xep_0100

View File

@ -0,0 +1,18 @@
XEP-0264: Jingle Content Thumbnails
===================================
.. module:: slixmpp.plugins.xep_0264
.. autoclass:: XEP_0264
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0264.stanza
:members:
:undoc-members:

View File

@ -0,0 +1,18 @@
XEP-0317: Hats
==============
.. module:: slixmpp.plugins.xep_0317
.. autoclass:: XEP_0317
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0317.stanza
:members:
:undoc-members:

View File

@ -0,0 +1,18 @@
XEP-0372: References
====================
.. module:: slixmpp.plugins.xep_0372
.. autoclass:: XEP_0372
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0372.stanza
:members:
:undoc-members:

View File

@ -0,0 +1,18 @@
XEP-0382: Spoiler Messages
==========================
.. module:: slixmpp.plugins.xep_0382
.. autoclass:: XEP_0382
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0382.stanza
:members:
:undoc-members:

View File

@ -0,0 +1,18 @@
XEP-0385: Stateless Inline Media Sharing (SIMS)
===============================================
.. module:: slixmpp.plugins.xep_0385
.. autoclass:: XEP_0385
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0385.stanza
:members:
:undoc-members:

View File

@ -0,0 +1,18 @@
XEP-0402: PEP Native Bookmarks
==============================
.. module:: slixmpp.plugins.xep_0402
.. autoclass:: XEP_0402
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0402.stanza
:members:
:undoc-members:

View File

@ -0,0 +1,18 @@
XEP-0446: File metadata element
===============================
.. module:: slixmpp.plugins.xep_0446
.. autoclass:: XEP_0446
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0446.stanza
:members:
:undoc-members:

View File

@ -0,0 +1,18 @@
XEP-0447: Stateless File Sharing
================================
.. module:: slixmpp.plugins.xep_0447
.. autoclass:: XEP_0447
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0447.stanza
:members:
:undoc-members:

View File

@ -0,0 +1,18 @@
XEP-0461: Message Replies
=========================
.. module:: slixmpp.plugins.xep_0461
.. autoclass:: XEP_0461
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0461.stanza
:members:
:undoc-members:

View File

@ -0,0 +1,17 @@
XEP-0469: Bookmark Pinning
==========================
.. module:: slixmpp.plugins.xep_0469
.. autoclass:: XEP_0469
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0469.stanza
:members:
:undoc-members:

View File

@ -0,0 +1,18 @@
XEP-0490: Message Displayed Synchronization
===========================================
.. module:: slixmpp.plugins.xep_0490
.. autoclass:: XEP_0490
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0490.stanza
:members:
:undoc-members:

View File

@ -1,6 +1,6 @@
XEP-0492: Chat Notification Settings
===========================
====================================
.. module:: slixmpp.plugins.xep_0492

View File

@ -10,8 +10,8 @@ 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://code.moparisthebest.com/moparisthebest/sendxmpp-py>`_
- `Groupchat <xmpp:xmpp-ircd@chatrooms.hackerposse.com?join>`_
- `Source <https://code.moparisthebest.com/moparisthebest/sendxmpp-py>`__
- `Groupchat <xmpp:xmpp-ircd@chatrooms.hackerposse.com?join>`__
Bots
----
@ -20,69 +20,65 @@ 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>`_
- `Source <https://git.khaganat.net/khaganat/BotLogMauve>`__
BukuBot
~~~~~~~
BukuBot makes it possible to manage and search your bookmarks from your chat.
- `Source <https://codeberg.org/sch/BukuBot>`_
- `Source <https://codeberg.org/sch/BukuBot>`__
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>`_
- `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>`_
- `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>`_
- `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>`_
- `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>`_
- `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/>`_
- `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>`_
- `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>`_
- `Source <https://github.com/immanuelfodor/xmpp-muc-message-gateway>`__
Services
--------
@ -91,14 +87,14 @@ 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>`_
- `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>`_
- `Groupchat <xmpp:slidge@conference.nicoco.fr?join>`__
- `Homepage <https://slidge.im/core/>`__
- `Source <https://sr.ht/~nicoco/slidge>`__

View File

@ -50,10 +50,39 @@ Running the event loop
only run for this amount of time, and if ``forever`` is False it will
run until disconnection).
This wrapper should be removed in slixmpp 1.9.0.
Therefore you can handle the event loop in any way you like
instead of using ``process()``.
Using connect()
~~~~~~~~~~~~~~~
:meth:`.XMLStream.connect` schedules a lot of things in the background, but that
only holds true if the event loop is running!
That is why in all examples we usually call connect() right before calling
a `loop.run_…` function, or the deprecated `process()` function.
Using a different event loop
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Immediately upon XMPP object creation (`ClientXMPP` / `ComponentXMPP`) you
should sets its `loop` attribute to whatever you want, and ideally this
should work. This path is less tested, so it may break, if that is the case
please report a bug.
Any access to the `loop` attribute if not user-initialized will set it
to the default asyncio event loop by default.
.. warning::
If the loop attribute is modified at runtime, the application will probably
end up in an hybrid state and asyncio may complain loudly that things bound
to an event loop are being ran in another. Try to avoid that situation.
Examples
~~~~~~~~
@ -73,10 +102,11 @@ callbacks while everything is not ready.
callback = lambda _: client.connected_event.set()
client.add_event_handler('session_start', callback)
client.connect()
loop = asyncio.get_event_loop()
loop.run_until_complete(event.wait())
# do some other stuff before running the event loop, e.g.
# loop.run_until_complete(httpserver.init())
client.process()
loop.run_forever()
Use with other asyncio-based libraries
@ -106,7 +136,7 @@ a simple <message>.
client.add_event_handler('session_start', get_pythonorg)
client.add_event_handler('session_start', get_asyncioorg)
client.connect()
client.process()
client.loop.run_until_complete(client.disconnected)
Blocking Iq
@ -136,6 +166,6 @@ JID indicating its findings.
client = ExampleClient('jid@example', 'password')
client.connect()
client.process()
client.loop.run_until_complete(client.disconnected)

View File

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

View File

@ -9,6 +9,7 @@
import logging
import hashlib
from asyncio import Future
from typing import Optional
from slixmpp import Message, Iq, Presence
@ -97,7 +98,7 @@ class ComponentXMPP(BaseXMPP):
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:
disable_starttls: Optional[bool] = None) -> Future:
"""Connect to the server.
@ -118,7 +119,7 @@ class ComponentXMPP(BaseXMPP):
self.server_name = self.boundjid.host
log.debug("Connecting to %s:%s", host, port)
XMLStream.connect(self, host=self.server_host, port=self.server_port, use_ssl=use_ssl)
return XMLStream.connect(self, host=self.server_host, port=self.server_port, use_ssl=use_ssl)
def incoming_filter(self, xml):
"""

View File

@ -122,6 +122,7 @@ PLUGINS = [
'xep_0447', # Stateless file sharing
'xep_0461', # Message Replies
'xep_0469', # Bookmarks Pinning
'xep_0482', # Call Invites
'xep_0490', # Message Displayed Synchronization
'xep_0492', # Chat Notification Settings
# Meant to be imported by plugins

View File

@ -162,7 +162,7 @@ class XEP_0009(BasePlugin):
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_call') > 1:
return
# Reply with error by default
error = self.client.plugin['xep_0009']._item_not_found(iq)
error = self.xmpp.plugin['xep_0009']._item_not_found(iq)
error.send()
def _on_jabber_rpc_method_response(self, iq, forwarded=False):
@ -175,7 +175,7 @@ class XEP_0009(BasePlugin):
"""
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_response') > 1:
return
error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
error = self.xmpp.plugin['xep_0009']._recipient_unvailable(iq)
error.send()
def _on_jabber_rpc_method_fault(self, iq, forwarded=False):
@ -188,7 +188,7 @@ class XEP_0009(BasePlugin):
"""
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_fault') > 1:
return
error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
error = self.xmpp.plugin['xep_0009']._recipient_unvailable(iq)
error.send()
def _on_jabber_rpc_error(self, iq, forwarded=False):
@ -201,7 +201,7 @@ class XEP_0009(BasePlugin):
"""
if not forwarded and self.xmpp.event_handled('jabber_rpc_error') > 1:
return
error = self.client.plugin['xep_0009']._recpient_unavailable(iq, iq.get_payload())
error = self.xmpp.plugin['xep_0009']._recipient_unvailable(iq)
error.send()
def _send_fault(self, iq, fault_xml): #

View File

@ -57,6 +57,9 @@ class XEP_0030(BasePlugin):
Given Given A single node
====== ======= ============================
Adding information for a given node without specifying the JID will
use the bound JID and therefore must be done after the bind happens.
Stream Handlers:
::

View File

@ -9,6 +9,7 @@ from typing import (
Set,
Tuple,
Union,
Dict,
)
from slixmpp.xmlstream import ElementBase, ET
@ -144,6 +145,25 @@ class DiscoInfo(ElementBase):
return True
return False
def dict_identities(self, lang: Optional[str] = None) -> Set[Dict[str, str]]:
"""
Return the set of all identities, each one as a dict with
category, type, xml_lang, and name keys.
:param lang: If there is a need to filter identities by lang.
"""
ids = self.get_identities(lang=lang, dedupe=True)
dict_ids = set()
for identity in ids:
dict_ids.add({
'category': identity[0],
'type': identity[1],
'xml_lang': identity[2],
'name': identity[3],
})
return dict_ids
def get_identities(self, lang: Optional[str] = None, dedupe: bool = True
) -> Iterable[IdentityType]:
"""

View File

@ -326,7 +326,10 @@ class XEP_0050(BasePlugin):
iq['command']['actions'] = actions
iq['command']['status'] = 'executing'
else:
iq['command']['actions'] = ['complete']
actions = ['complete']
if session['allow_prev']:
actions.append('prev')
iq['command']['actions'] = actions
iq['command']['status'] = 'executing'
iq['command']['notes'] = session['notes']

View File

@ -464,10 +464,10 @@ class XEP_0060(BasePlugin):
"""
Retrieve the ItemIDs hosted by a given node, using disco.
"""
self.xmpp['xep_0030'].get_items(jid, node, ifrom=ifrom,
callback=callback, timeout=timeout,
iterator=iterator,
timeout_callback=timeout_callback)
return self.xmpp['xep_0030'].get_items(jid, node, ifrom=ifrom,
callback=callback, timeout=timeout,
iterator=iterator,
timeout_callback=timeout_callback)
def modify_affiliations(self, jid, node, affiliations=None, ifrom=None,
timeout_callback=None, callback=None,

View File

@ -48,8 +48,12 @@ class XEP_0223(BasePlugin):
:param node: Node to set the configuration at.
"""
config = self.xmpp['xep_0004'].Form()
config = self.xmpp['xep_0004'].stanza.Form()
config['type'] = 'submit'
config.add_field(
var='FORM_TYPE',
ftype='hidden',
value='http://jabber.org/protocol/pubsub#node_config')
for field, value in self.profile.items():
config.add_field(var=field, value=value)

View File

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

View File

@ -1,11 +1,12 @@
# 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 permissio
import logging
from typing import Optional
from slixmpp.stanza import Message
from slixmpp.jid import JID
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin
@ -49,3 +50,54 @@ class XEP_0308(BasePlugin):
def _handle_correction(self, msg: Message):
self.xmpp.event('message_correction', msg)
def build_correction(self, id_to_replace: str, mto: JID,
mfrom: Optional[JID] = None, mtype: str = 'chat',
mbody: str = '') -> Message:
"""
Build a corrected message.
:param id_to_replace: The id of the original message.
:param mto: Recipient of the message, must be the same as the original
message.
:param mfrom: Sender of the message, must be the same as the original
message.
:param mtype: Type of the message, must be the send as the original
message.
:param mbody: The corrected message body.
"""
msg = self.xmpp.make_message(
mto=mto,
mfrom=mfrom,
mbody=mbody,
mtype=mtype
)
msg['replace']['id'] = id_to_replace
return msg
def correct_message(self, msg: Message, body: str) -> Message:
"""
Send a correction to an existing message.
:param msg: The message that must be replaced.
:param body: The body to set in the correcting message.
:returns: The message that was sent.
"""
to_replace = msg['id']
mto = msg['to']
mfrom = msg['from']
mtype = msg['type']
if not to_replace:
raise ValueError('No available ID for replacing the message')
if not mto:
raise ValueError('No available recipient JID')
new = self.build_correction(
id_to_replace=to_replace,
mto=mto,
mfrom=mfrom,
mtype=mtype,
mbody=body,
)
new.send()
return new

View File

@ -138,10 +138,10 @@ class XEP_0356(BasePlugin):
Raises ValueError if the server did not advertise the corresponding privileges
:param jid: user we want to add or modify roster items
:param roster_items: a dict containing the roster items' JIDs as keys and
nested dicts containing names, subscriptions and groups.
Example:
Here is an example of a roster_items value:
.. code-block:: json
{
"friend1@example.com": {
"name": "Friend 1",
@ -152,8 +152,13 @@ class XEP_0356(BasePlugin):
"name": "Friend 2",
"subscription": "from",
"groups": ["group3"],
},
}
},
}
:param jid: user we want to add or modify roster items
:param roster_items: a dict containing the roster items' JIDs as keys and
nested dicts containing names, subscriptions and groups.
"""
if isinstance(jid, str):
jid = JID(jid)

View File

@ -14,11 +14,12 @@ class Reply(ElementBase):
interfaces = {"id", "to"}
def add_quoted_fallback(self, fallback: str, nickname: Optional[str] = None):
"""
r"""
Add plain text fallback for clients not implementing XEP-0461.
``msg["reply"].add_quoted_fallback("Some text", "Bob")`` will
prepend "> Bob:\n> Some text\n" to the body of the message, and set the
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.

View File

@ -0,0 +1,11 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 Mathieu Pasquet
# This file is part of Slixmpp.
# See the file LICENSE for copying permissio
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0482 import stanza
from slixmpp.plugins.xep_0482.call_invites import XEP_0482
register_plugin(XEP_0482)

View File

@ -0,0 +1,55 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 Mathieu Pasquet
# This file is part of Slixmpp.
# See the file LICENSE for copying permissio
import logging
from typing import Optional
from slixmpp.stanza import Message
from slixmpp.jid import JID
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0482 import stanza
log = logging.getLogger(__name__)
class XEP_0482(BasePlugin):
"""
XEP-0482: Call Invites
This plugin defines the stanza elements for Call Invites, as well as new
events:
- `call-invite`
- `call-reject`
- `call-retract`
- `call-leave`
- `call-left`
"""
name = 'xep_0482'
description = 'XEP-0482: Call Invites'
dependencies = set()
stanza = stanza
def plugin_init(self):
stanza.register_plugins()
for event in ('invite', 'reject', 'retract', 'leave', 'left'):
self.xmpp.register_handler(
Callback(f'Call {event}',
StanzaPath(f'message/call-{event}'),
self._handle_event))
def _handle_event(self, message):
for event in ('invite', 'reject', 'retract', 'leave', 'left'):
if message.get_plugin(f'call-{event}', check=True):
self.xmpp.event(f'call-{event}')
def plugin_end(self):
for event in ('invite', 'reject', 'retract', 'leave', 'left'):
self.xmpp.remove_handler(f'Call {event}')

View File

@ -0,0 +1,102 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 Mathieu Pasquet
# This file is part of Slixmpp.
# See the file LICENSE for copying permission
from typing import Tuple, List, Optional
from slixmpp import Message
from slixmpp.jid import JID
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
NS = 'urn:xmpp:call-invites:0'
class Jingle(ElementBase):
name = 'jingle'
namespace = NS
plugin_attrib = 'jingle'
plugin_multi_attrib = 'jingles'
interfaces = {'sid', 'jid'}
def set_jid(self, value: JID) -> None:
if not isinstance(value, JID):
try:
value = JID(value)
except ValueError:
raise ValueError(f'"jid" must be a valid JID object')
self.xml.attrib['jid'] = value.full
def get_jid(self) -> Optional[JID]:
try:
return JID(self.xml.attrib.get('jid', ''))
except ValueError:
return None
class External(ElementBase):
name = 'external'
namespace = NS
plugin_attrib = 'external'
plugin_multi_attrib = 'externals'
interfaces = {'uri'}
class Invite(ElementBase):
name = 'invite'
namespace = NS
plugin_attrib = 'call-invite'
interfaces = {'video'}
def get_methods(self) -> Tuple[List[Jingle], List[External]]:
return (self['jingles'], self['externals'])
def set_video(self, value: bool) -> None:
if not isinstance(value, bool):
raise ValueError(f'Invalid value for the video attribute: {value}')
self.xml.attrib['video'] = str(value).lower()
def get_video(self) -> bool:
vid = self.xml.attrib.get('video', 'false').lower()
return vid == 'true'
class Retract(ElementBase):
name = 'retract'
namespace = NS
plugin_attrib = 'call-retract'
interfaces = {'id'}
class Accept(ElementBase):
name = 'accept'
namespace = NS
plugin_attrib = 'call-accept'
interfaces = {'id'}
class Reject(ElementBase):
name = 'reject'
namespace = NS
plugin_attrib = 'call-reject'
interfaces = {'id'}
class Left(ElementBase):
name = 'left'
namespace = NS
plugin_attrib = 'call-left'
interfaces = {'id'}
def register_plugins() -> None:
register_stanza_plugin(Message, Invite)
register_stanza_plugin(Message, Retract)
register_stanza_plugin(Message, Accept)
register_stanza_plugin(Message, Reject)
register_stanza_plugin(Message, Left)
register_stanza_plugin(Invite, Jingle, iterable=True)
register_stanza_plugin(Invite, External, iterable=True)
register_stanza_plugin(Accept, Jingle)
register_stanza_plugin(Accept, External)

View File

@ -375,7 +375,23 @@ class XMLStream(asyncio.BaseProtocol):
@property
def loop(self) -> AbstractEventLoop:
if self._loop is None:
self._loop = asyncio.get_event_loop()
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
self._loop = asyncio.get_event_loop()
# We do not know what exception will be raised in the future
# instead of the warning
except Exception:
try:
current = asyncio.get_running_loop()
except RuntimeError:
current = None
if current is not None:
self._loop = current
else:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self._loop = loop
return self._loop
@loop.setter
@ -409,9 +425,10 @@ class XMLStream(asyncio.BaseProtocol):
self.disconnected.set_result(True)
self.disconnected = asyncio.Future()
def connect(self, host: str = '', port: int = 0, use_ssl: Optional[bool] = None,
def connect(self, host: str = '', port: int = 0,
use_ssl: Optional[bool] = None,
force_starttls: Optional[bool] = None,
disable_starttls: Optional[bool] = None) -> None:
disable_starttls: Optional[bool] = None) -> asyncio.Future:
"""Create a new socket and connect to the server.
:param host: The name of the desired server for the connection.
@ -430,6 +447,7 @@ class XMLStream(asyncio.BaseProtocol):
upgrade to TLS, even if the server provides
it. Use this for example if youre on
localhost
:returns: A future on the current connection attempt
"""
if self._run_out_filters is None or self._run_out_filters.done():
@ -461,8 +479,14 @@ class XMLStream(asyncio.BaseProtocol):
self._connect_routine(),
loop=self.loop,
)
return self._current_connection_attempt
async def _connect_routine(self) -> None:
async def _connect_routine(self) -> Optional[asyncio.Future]:
"""
Returns None if the attempt was canceled or if the connection succeeded
(cancelling done manually by the library user, so that should be known)
or the next connection attempt future if a new try has been scheduled.
"""
self.event_when_connected = "connected"
if self._connect_loop_wait > 0:
@ -487,7 +511,7 @@ class XMLStream(asyncio.BaseProtocol):
ssl_context = None
if self._current_connection_attempt is None:
return
return None
try:
server_hostname = self.default_domain if self.use_ssl else None
await self.loop.create_connection(lambda: self,
@ -499,11 +523,12 @@ class XMLStream(asyncio.BaseProtocol):
except Socket.gaierror as e:
self.event('connection_failed',
'No DNS record available for %s' % self.default_domain)
self.reschedule_connection_attempt()
return self.reschedule_connection_attempt()
except OSError as e:
log.debug('Connection failed: %s', e)
self.event("connection_failed", e)
self.reschedule_connection_attempt()
return self.reschedule_connection_attempt()
return None
def process(self, *, forever: bool = True, timeout: Optional[int] = None) -> None:
"""Process all the available XMPP events (receiving or sending data on the
@ -639,19 +664,22 @@ class XMLStream(asyncio.BaseProtocol):
self._set_disconnected_future()
self.event("disconnected", self.disconnect_reason or exception)
def reschedule_connection_attempt(self) -> None:
def reschedule_connection_attempt(self) -> Optional[asyncio.Future]:
"""
Increase the exponential back-off and initate another background
_connect_routine call to connect to the server.
:returns: A future on the next scheduled connection attempt.
"""
# abort if there is no ongoing connection attempt
if self._current_connection_attempt is None:
return
return None
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,
)
return self._current_connection_attempt
def cancel_connection_attempt(self) -> None:
"""

View File

@ -0,0 +1,26 @@
import unittest
from slixmpp import Message
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0308 import Replace
from slixmpp.xmlstream import register_stanza_plugin
class TestCorrectStanza(SlixTest):
def setUp(self):
register_stanza_plugin(Message, Replace)
def testBuild(self):
"""Test that the element is created correctly."""
msg = Message()
msg['type'] = 'chat'
msg['replace']['id'] = 'toto123'
self.check(msg, """
<message type="chat">
<replace xmlns="urn:xmpp:message-correct:0" id="toto123"/>
</message>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestCorrectStanza)

View File

@ -0,0 +1,42 @@
import unittest
from slixmpp import Message
from slixmpp.jid import JID
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0482 import stanza
from slixmpp.plugins.xep_0482.stanza import External, Jingle
from slixmpp.xmlstream import register_stanza_plugin
class TestCallInviteStanza(SlixTest):
def setUp(self):
stanza.register_plugins()
def test_invite(self):
"""Test that the element is created correctly."""
msg = Message()
msg['call-invite']['video'] = True
jingle = Jingle()
jingle['sid'] = 'toto'
jingle['jid'] = JID('toto@example.com/m')
external = External()
external['uri'] = "https://example.com/call"
msg['call-invite'].append(jingle)
msg['call-invite'].append(external)
self.check(msg, """
<message>
<invite xmlns="urn:xmpp:call-invites:0" video="true">
<jingle sid="toto" jid="toto@example.com/m" />
<external uri="https://example.com/call" />
</invite>
</message>
""")
self.assertEqual(
msg['call-invite'].get_methods(),
([jingle], [external]),
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestCallInviteStanza)

View File

@ -0,0 +1,28 @@
import unittest
from slixmpp.test import SlixTest
class TestPrivatePEP(SlixTest):
def testConfigureNode(self):
self.stream_start(mode='client', plugins=['xep_0223'])
self.xmpp.plugin['xep_0223'].configure(node="toto")
self.send("""
<iq type="set" id="1">
<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
<configure node='toto'>
<x xmlns='jabber:x:data' type='submit'>
<field var='FORM_TYPE' type='hidden'>
<value>http://jabber.org/protocol/pubsub#node_config</value>
</field>
<field var='pubsub#persist_items'><value>1</value></field>
<field var='pubsub#access_model'><value>whitelist</value></field>
</x>
</configure>
</pubsub>
</iq>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestPrivatePEP)

View File

@ -0,0 +1,53 @@
import unittest
from slixmpp.jid import JID
from slixmpp.test import SlixTest
class TestStreamCorrect(SlixTest):
def test_recv_correct(self):
self.stream_start(mode='client', plugins=['xep_0308'])
recv = []
def recv_correct(msg):
recv.append(msg)
self.xmpp.add_event_handler('message_correction', recv_correct)
self.recv("""
<message from="example.com" to="toto@example">
<replace xmlns="urn:xmpp:message-correct:0" id="tototo"/>
<body>oucou</body>
</message>
""")
received = recv[0]
self.assertEqual(received['replace']['id'], "tototo")
def test_send_correct(self):
self.stream_start(mode='client', plugins=['xep_0308'])
corrected = self.xmpp.plugin['xep_0308'].build_correction(
id_to_replace="12345",
mto=JID('toto@example.com'),
mbody="I am replacing",
)
self.assertEqual(corrected['replace']['id'], '12345')
self.assertEqual(corrected['to'], JID('toto@example.com'))
self.assertEqual(corrected['body'], 'I am replacing')
corrected['id'] = 'my id'
corrected = self.xmpp.plugin['xep_0308'].correct_message(
corrected,
'This is new',
)
self.send("""
<message type="chat" to="toto@example.com">
<body>This is new</body>
<replace xmlns="urn:xmpp:message-correct:0" id="my id" />
</message>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamCorrect)