This commit is contained in:
2026-08-02 18:57:40 +02:00
parent 5e5ab8681a
commit 6fa59321c0
5759 changed files with 712133 additions and 66 deletions
@@ -0,0 +1,262 @@
########################################################################
# File name: __init__.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
"""
:mod:`~aioxmpp.muc` --- Multi-User-Chat support (:xep:`45`)
###########################################################
This subpackage provides client-side support for :xep:`0045`.
.. versionadded:: 0.5
.. versionchanged:: 0.9
Nearly the whole public interface of this module has been re-written in
0.9 to make it coherent with the Modern IM interface defined by
:class:`aioxmpp.im`.
Using Multi-User-Chats
======================
To start using MUCs in your application, you have to load the :class:`Service`
into the client, using :meth:`~.node.Client.summon`.
.. currentmodule:: aioxmpp
.. autoclass:: MUCClient
.. currentmodule:: aioxmpp.muc
.. class:: Service
Alias of :class:`.MUCClient`.
.. deprecated:: 0.8
The alias will be removed in 1.0.
The service returns :class:`Room` objects which are used to track joined MUCs:
.. autoclass:: Room
.. autoclass:: RoomState
.. autoclass:: LeaveMode
Inside rooms, there are occupants:
.. autoclass:: Occupant
.. autoclass:: ServiceMember
Timeout controls / :xep:`0410` (MUC Self-Ping) support
------------------------------------------------------
.. versionadded:: 0.11
:xep:`410` support and aliveness detection.
Motivation
^^^^^^^^^^
In XMPP, multi-user chat services may reside on a server different than the
one the user is at. This may either be due to the service running on a remote
domain, or due to the service being connected via the network to the users
server as component (see e.g. :xep:`114`).
When the connection between the MUC service and the users server is broken
when stanzas need to be delivered, stanzas can be lost. This can lead to the
MUC getting "out of sync", in the sense that different participants have
different views of what happens and who even is in the MUC; this uncertainty
can go as far as a client assuming that theyre still joined, while they were
long removed from the MUC.
These types of breakages are hard to detect, unless the user tries to send a
message through the MUC (in which case the lack of reflection or an error reply
will give a clue that something is wrong). In the worst case, with an always-on
client, it may appear that the MUC has been silent for days, while in fact
everyone has been chatting away happily.
Solution
^^^^^^^^
The underlying problem (networks get split) cannot be solved. While Stream
Management on the s2s links could mitigate the issue to some extent, there will
always be limits and circumstances at play which can still cause the
out-of-sync situation.
While loss of messages can be compensated for by fetching the messages from the
archive (doing this automatically on an interruption is out of scope for
aioxmpp), there is no way for an application to detect that the client has been
removed from the MUC except by explicitly pinging or sending messages.
To codify the complex rules which are needed to silently (i.e. invisible to
other participants) check whether a client is still joined, :xep:`410` was
written. It specifies the use of :xep:`199` pings through the MUC to the
clients occupant (i.e. pinging oneself). MUC services explicitly reject the
ping request if the sending client is not an occupant.
.. _api-aioxmpp.muc-self-ping-logic:
Self-Ping Implementation and Logic
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The :xep:`410` implementation in aioxmpp is controlled with several attributes
and lines of defense. In the first line of defense, there is a
:class:`~aioxmpp.utils.AlivenessMonitor` instance (this class is also used to
manage pinging the main XML stream). It is configured through
:attr:`aioxmpp.muc.Room.muc_soft_timeout` and
:attr:`~aioxmpp.muc.Room.muc_hard_timeout`.
The two timers run concurrently. When the soft timeout expires, the pinger (see
below) task is started. When the hard timeout expires, the MUC is marked stale
(this means, the :meth:`~aioxmpp.muc.Room.on_muc_stale` event fires). The
timers for both timeouts are reset whenever a presence or message stanza is
received from the MUC, preventing unnecessary pinging.
The pinger task emits pings in a defined interval
(:attr:`~aioxmpp.muc.Room.muc_ping_interval`). The pings have a timeout of
:attr:`~aioxmpp.muc.Room.muc_ping_timeout`. If a ping is replied to, the result
is interpreted according to :xep:`410`. If the result is positive (= user
still joined), the soft and hard timeout timers mentioned above are reset
(the pinger, thus, ideally prevents the hard timeout from being triggered if
the connection to the MUC is fine after the soft timeout expired). If the
result is inconclusive, pinging continues. If the result is negative (= user
is not joined anymore), the MUC room is marked as exited (with the reason
:attr:`~aioxmpp.muc.LeaveMode.DISCONNECTED`), except if it is set to
autorejoin, in which case a re-join (just as if the XML stream had been
disconnected) is attempted.
The default timeouts are set reasonably high to work reliably even on mobile
links.
.. warning::
Please see the notes on :attr:`~aioxmpp.muc.Room.muc_ping_timeout`
when changing the value of :attr:`~aioxmpp.muc.Room.muc_ping_timeout` or
:attr:`~aioxmpp.muc.Room.muc_ping_interval`.
Forms
=====
.. autoclass:: ConfigurationForm
:members:
.. autoclass:: InfoForm
:members:
.. autoclass:: VoiceRequestForm
:members:
XSOs
====
.. autoclass:: StatusCode
.. currentmodule:: aioxmpp.muc.xso
Attributes added to existing XSOs
---------------------------------
.. attribute:: aioxmpp.Message.xep0045_muc
A :class:`GenericExt` object or :data:`None`.
.. attribute:: aioxmpp.Message.xep0045_muc_user
A :class:`UserExt` object or :data:`None`.
.. attribute:: aioxmpp.Presence.xep0045_muc
A :class:`GenericExt` object or :data:`None`.
.. attribute:: aioxmpp.Presence.xep0045_muc_user
A :class:`UserExt` object or :data:`None`.
.. attribute:: aioxmpp.Message.xep0249_direct_invite
A :class:`DirectInvite` object or :data:`None`.
Generic namespace
-----------------
.. autoclass:: GenericExt
.. autoclass:: History
User namespace
--------------
.. autoclass:: UserExt
.. autoclass:: Status
.. autoclass:: DestroyNotification
.. autoclass:: Decline
.. autoclass:: Invite
.. autoclass:: UserItem
.. autoclass:: UserActor
.. autoclass:: Continue
Admin namespace
---------------
.. autoclass:: AdminQuery
.. autoclass:: AdminItem
.. autoclass:: AdminActor
Owner namespace
---------------
.. autoclass:: OwnerQuery
.. autoclass:: DestroyRequest
:xep:`249` Direct Invitations
-----------------------------
.. autoclass:: DirectInvite
"""
from .service import ( # NOQA: F401
MUCClient,
Occupant,
Room,
LeaveMode,
RoomState,
ServiceMember,
)
from . import xso # NOQA: F401
from .xso import ( # NOQA: F401
ConfigurationForm,
InfoForm,
VoiceRequestForm,
StatusCode,
)
Service = MUCClient # NOQA
@@ -0,0 +1,519 @@
########################################################################
# File name: self_ping.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
import asyncio
import random
import time
from datetime import timedelta
import aioxmpp.errors
import aioxmpp.ping
import aioxmpp.stream
import aioxmpp.structs
import aioxmpp.utils
def _apply_jitter(v, amplitude):
return v * ((random.random() * 2 - 1) * amplitude + 1)
class MUCPinger:
"""
:param on_fresh: Called when the pinger finds evidence that the user is
connected
:param on_exited: Called when the pinger finds evidence that the user is
disconnected
:param loop: Event loop to use
This class manages a coroutine which sends pings to a remote entity and
interprets the results according to :xep:`410`.
If the result of a ping indicates that the client is not joined in the MUC
anymore, `on_exited` is called. If the result of a ping indicates that the
client is still joined in a MUC, `on_stale` is called. If the result are
inconclusive, no call is made.
A ping result does *not* imply a call to :meth:`stop`. The callbacks are
called on each ping response, thus, on average up to once each
:attr:`ping_interval` until :meth:`stop` is called.
Pings are sent once each :attr:`ping_interval` (see there for details on
the effects of changing the interval while the pinger is running). If
:attr:`ping_interval` is less than :attr:`ping_timeout`, it is possible
that multiple pings are in-flight at the same time (this is handled
correctly). Take into account that resources for tracking up to
:attr:`ping_timeout` divided by :attr:`ping_interval` IQ responses will be
required.
To start the pinger, :meth:`start` must be called.
.. automethod:: start
.. automethod:: stop
.. attribute:: ping_address
The address pings are sent to.
This can be changed while the pinger is running. Changes take effect
when the next ping is sent. Already in-flight pings are not affected.
.. autoattribute:: ping_interval
.. autoattribute:: ping_timeout
"""
def __init__(self, ping_address, client, on_fresh, on_exited, logger, loop):
super().__init__()
self.ping_address = ping_address
self._ping_interval = timedelta(minutes=2)
self._ping_timeout = timedelta(minutes=8)
self._client = client
self._on_fresh = on_fresh
self._on_exited = on_exited
self._loop = loop
self._logger = logger
self._task = None
@property
def ping_interval(self) -> timedelta:
"""
The interval at which pings are sent.
While the pinger is running, every `ping_interval` a new ping is
started. Each ping has its individual :attr:`ping_timeout`.
Changing this property takes effect after the next ping has been sent.
Thus, if the :attr:`ping_interval` was set to one day and is then
changed to one minute, it takes up to a day until the one minute
interval starts being used.
"""
return self._ping_interval
@ping_interval.setter
def ping_interval(self, value: timedelta):
# cheap & duck-typey enforcement of timedelta compatibility
self._ping_interval = value + timedelta()
@property
def ping_timeout(self) -> timedelta:
"""
The maximum time to wait for a reply to a ping.
Each ping sent by the pinger has its individual timeout, based on this
property at the time the ping is sent.
"""
return self._ping_timeout
@ping_timeout.setter
def ping_timeout(self, value: timedelta):
# cheap & duck-typey enforcement of timedelta compatibility
self._ping_timeout = value + timedelta()
def start(self):
"""
Start the pinging coroutine using the client and event loop which was
passed to the constructor.
:meth:`start` always behaves as if :meth:`stop` was called right before
it.
"""
self._logger.debug("%s: request to start pinger",
self.ping_address)
self.stop()
self._task = asyncio.ensure_future(self._pinger(), loop=self._loop)
def stop(self):
"""
Stop the pinger (if it is running) and discard all data on in-flight
pings.
This method will do nothing if the pinger is already stopped. It is
idempotent.
"""
self._logger.debug("%s: request to stop pinger",
self.ping_address)
if self._task is None:
self._logger.debug("%s: already stopped", self.ping_address)
return
self._logger.debug("%s: sending cancel signal", self.ping_address)
self._task.cancel()
self._task = None
def _interpret_result(self, task):
"""
Interpret the result of a ping.
:param task: The pinger task.
The result or exception of the `task` is interpreted as follows:
* :data:`None` result: *positive*
* :class:`aioxmpp.errors.XMPPError`, ``service-unavailable``:
*positive*
* :class:`aioxmpp.errors.XMPPError`, ``feature-not-implemented``:
*positive*
* :class:`aioxmpp.errors.XMPPError`, ``item-not-found``: *inconclusive*
* :class:`aioxmpp.errors.XMPPError`, ``remote-server-not-found``:
*inconclusive*
* :class:`aioxmpp.errors.XMPPError`, ``remote-server-timeout``:
*inconclusive*
* :class:`aioxmpp.errors.XMPPError`: *negative*
* :class:`asyncio.TimeoutError`: *inconclusive*
* Any other exception: *inconclusive*
"""
if task.exception() is None:
self._logger.debug("%s: ping reply has no error -> emitting fresh "
"event", self.ping_address)
self._on_fresh()
return
exc = task.exception()
if isinstance(exc, aioxmpp.errors.XMPPError):
if exc.condition in [
aioxmpp.errors.ErrorCondition.SERVICE_UNAVAILABLE,
aioxmpp.errors.ErrorCondition.FEATURE_NOT_IMPLEMENTED]:
self._logger.debug(
"%s: ping reply has error indicating freshness: %s",
self.ping_address,
exc.condition,
)
self._on_fresh()
return
if exc.condition in [
aioxmpp.errors.ErrorCondition.ITEM_NOT_FOUND,
aioxmpp.errors.ErrorCondition.REMOTE_SERVER_NOT_FOUND,
aioxmpp.errors.ErrorCondition.REMOTE_SERVER_TIMEOUT]:
self._logger.debug(
"%s: ping reply has inconclusive error: %s",
self.ping_address,
exc.condition,
)
return
self._logger.debug(
"%s: ping reply has error indicating that the client got "
"removed: %s",
self.ping_address,
exc.condition,
)
self._on_exited()
async def _pinger(self):
in_flight = []
next_ping_at = None
self._logger.debug("%s: pinger booted up", self.ping_address)
try:
while True:
self._logger.debug("%s: pinger loop. interval=%r",
self.ping_address,
self.ping_interval)
now = time.monotonic()
ping_interval = self.ping_interval.total_seconds()
if next_ping_at is None:
next_ping_at = now - 1
timeout = next_ping_at - now
if timeout <= 0:
# do not send pings while the client is in suspended state
# (= Stream Management hibernation). This will only add to
# the queue for no good reason, we wont get any reply soon
# anyways.
if self._client.suspended:
self._logger.debug(
"%s: omitting self-ping, as the stream is "
"currently hibernated",
self.ping_address,
)
else:
self._logger.debug(
"%s: sending self-ping with timeout %r",
self.ping_address,
self.ping_timeout,
)
in_flight.append(asyncio.ensure_future(
asyncio.wait_for(
aioxmpp.ping.ping(self._client,
self.ping_address),
self.ping_timeout.total_seconds()
)
))
next_ping_at = now + _apply_jitter(ping_interval, 0.1)
timeout = ping_interval
assert timeout > 0
if not in_flight:
self._logger.debug(
"%s: pinger has nothing to do, sleeping for %s",
self.ping_address,
timeout,
)
await asyncio.sleep(timeout)
continue
self._logger.debug(
"%s: pinger waiting for %d pings for at most %ss",
self.ping_address,
len(in_flight),
timeout,
)
done, pending = await asyncio.wait(
in_flight,
timeout=timeout,
return_when=asyncio.FIRST_COMPLETED,
)
for fut in done:
self._interpret_result(fut)
in_flight = list(pending)
finally:
self._logger.debug("%s: pinger exited", self.ping_address,
exc_info=True)
for fut in in_flight:
if not fut.done():
fut.cancel()
class MUCMonitor:
"""
:param ping_address: Address to send pings to. Can be changed later with
:attr:`ping_address`.
:type ping_address: :class:`aioxmpp.JID`
:param client: Client to send pings with.
:type stream: :class:`aioxmpp.stream.StanzaStream`
:param on_stale: Called when the pinger detects stale state.
:param on_fresh: Called when the pinger detects fresh state.
:param on_exited: Called when the pinger detects that the user is not in
the room anymore.
:param loop: Event loop to use (defaults to the current event loop)
.. automethod:: enable
.. automethod:: disable
.. automethod:: reset
.. attribute:: ping_address
The address to ping.
.. autoattribute:: stream
.. autoattribute:: is_stale
.. autoattribute:: soft_timeout
.. autoattribute:: hard_timeout
.. autoattribute:: ping_interval
.. autoattribute:: ping_timeout
"""
def __init__(self,
ping_address: aioxmpp.structs.JID,
client: "aioxmpp.node.Client",
on_stale,
on_fresh,
on_exited,
logger,
loop=None):
loop = loop or asyncio.get_event_loop()
super().__init__()
self._client = client
self._is_stale = False
self.on_stale = on_stale
self.on_fresh = on_fresh
self.on_exited = on_exited
self._soft_timeout = timedelta(minutes=13)
self._hard_timeout = timedelta(minutes=15)
self._monitor = aioxmpp.utils.AlivenessMonitor(loop)
# disable the monitor altogether
self._monitor.deadtime_hard_limit = None
self._monitor.deadtime_soft_limit = None
self._monitor_enabled = False
self._monitor.on_deadtime_hard_limit_tripped.connect(
self._hard_limit_tripped
)
self._monitor.on_deadtime_soft_limit_tripped.connect(
self._soft_limit_tripped
)
self._logger = logger
self._pinger = MUCPinger(
ping_address,
client,
self._pinger_fresh_detected,
self._pinger_exited_detected,
logger,
loop,
)
self.ping_address = ping_address
@property
def is_stale(self) -> bool:
return self._is_stale
@property
def soft_timeout(self) -> timedelta:
return self._soft_timeout
@soft_timeout.setter
def soft_timeout(self, new_value: timedelta):
# cheap & duck-typey enforcement of timedelta compatibility
self._soft_timeout = new_value + timedelta()
if self._monitor_enabled:
self._monitor.deadtime_soft_limit = new_value
@property
def hard_timeout(self) -> timedelta:
return self._hard_timeout
@hard_timeout.setter
def hard_timeout(self, new_value: timedelta):
# cheap & duck-typey enforcement of timedelta compatibility
self._hard_timeout = new_value + timedelta()
if self._monitor_enabled:
self._monitor.deadtime_hard_limit = new_value
ping_address = aioxmpp.utils.proxy_property(
"_pinger",
"ping_address",
)
ping_timeout = aioxmpp.utils.proxy_property(
"_pinger",
"ping_timeout",
)
ping_interval = aioxmpp.utils.proxy_property(
"_pinger",
"ping_interval",
)
def enable(self):
"""
Enable the monitor, if it is not enabled already.
If the monitor is not already enabled, the aliveness timeouts are reset
and configured and the stale state is cleared.
"""
self._logger.debug("%s: request to enable monitoring",
self.ping_address)
if self._monitor_enabled:
return
self._is_stale = False
self._enable_monitor()
def disable(self):
"""
Disable the monitor.
Reset and stop the aliveness timeouts. Cancel and stop pinging.
"""
self._disable_monitor()
self._pinger.stop()
def reset(self):
"""
Reset the monitor.
Reset the aliveness timeouts. Clear the stale state. Cancel and stop
pinging.
Call `on_fresh` if the stale state was set.
"""
self._monitor.notify_received()
self._pinger.stop()
self._mark_fresh()
def _mark_stale(self):
"""
- Emit on_stale if stale flag is cleared
- Set stale flag
"""
if not self._is_stale:
self._logger.debug("%s: transition to stale", self.ping_address)
self.on_stale()
self._is_stale = True
def _mark_fresh(self):
"""
- Emit on_fresh if stale flag is set
- Clear stale flag
"""
if self._is_stale:
self._logger.debug("%s: transition to fresh", self.ping_address)
self.on_fresh()
self._is_stale = False
def _enable_monitor(self):
# we need to call notify received *first* to prevent spurious events
self._monitor.notify_received()
self._monitor.deadtime_soft_limit = self._soft_timeout
self._monitor.deadtime_hard_limit = self._hard_timeout
self._monitor_enabled = True
self._logger.debug("%s: enabled monitoring: "
"soft_timeout=%r "
"hard_timeout=%r "
"ping_interval=%r "
"ping_timeout=%r",
self.ping_address,
self._soft_timeout,
self._hard_timeout,
self.ping_interval,
self.ping_timeout)
def _disable_monitor(self):
# we need to call notify received *first* to prevent spurious events
self._monitor.notify_received()
self._monitor.deadtime_soft_limit = None
self._monitor.deadtime_hard_limit = None
self._monitor_enabled = False
self._logger.debug("%s: disabled monitoring", self.ping_address)
def _pinger_fresh_detected(self):
self._logger.debug("%s: fresh detected", self.ping_address)
self._pinger.stop()
self._monitor.notify_received()
self._mark_fresh()
def _pinger_exited_detected(self):
self._logger.debug("%s: exited detected", self.ping_address)
self._pinger.stop()
self.on_exited()
def _soft_limit_tripped(self):
self._logger.debug("%s: soft-limit tripped, starting pinger",
self.ping_address)
self._pinger.start()
def _hard_limit_tripped(self):
self._logger.debug("%s: hard-limit tripped, marking stale",
self.ping_address)
self._mark_stale()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,781 @@
########################################################################
# File name: xso.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
import enum
import aioxmpp.forms
import aioxmpp.stanza
import aioxmpp.stringprep
import aioxmpp.xso as xso
from aioxmpp.utils import namespaces
namespaces.xep0045_muc = "http://jabber.org/protocol/muc"
namespaces.xep0045_muc_user = "http://jabber.org/protocol/muc#user"
namespaces.xep0045_muc_admin = "http://jabber.org/protocol/muc#admin"
namespaces.xep0045_muc_owner = "http://jabber.org/protocol/muc#owner"
namespaces.xep0249_conference = "jabber:x:conference"
class StatusCode(enum.IntEnum):
"""
This integer enumeration (see :class:`enum.IntEnum`) is used for the
status codes defined in :xep:`45`.
Note that members of this enumeration are equal to their respective integer
values, making it ideal for backward- and forward-compatible code and a
replacement for magic numbers.
.. versionadded:: 0.10
Before version 0.10, this enum did not exist and the numeric codes
were used bare. Since this is an :class:`~enum.IntEnum`, it is possible
to use the named enum members and their numeric codes interchangeably.
.. attribute:: NON_ANONYMOUS
:annotation: = 100
Included when entering a room where every user can see every users
real JID.
.. attribute:: AFFILIATION_CHANGE
:annotation: = 101
Included in out-of-band messages informing about affiliation changes.
.. attribute:: SHOWING_UNAVAILABLE
:annotation: = 102
Inform occupants that room now shows unavailable members.
.. attribute:: NOT_SHOWING_UNAVAILABLE
:annotation: = 103
Inform occupants that room now does not show unavailable members.
.. attribute:: CONFIG_NON_PRIVACY_RELATED
:annotation: = 104
Inform occupants that a non-privacy related configuration change has
occurred.
.. attribute:: SELF
:annotation: = 110
Inform that the stanza refers to the addressee themselves.
.. attribute:: CONFIG_ROOM_LOGGING
:annotation: = 170
Inform that the room is now logged.
.. attribute:: CONFIG_NO_ROOM_LOGGING
:annotation: = 171
Inform that the room is not logged anymore.
.. attribute:: CONFIG_NON_ANONYMOUS
:annotation: = 172
Inform that the room is now not anonymous.
.. attribute:: CONFIG_SEMI_ANONYMOUS
:annotation: = 173
Inform that the room is now semi-anonymous.
.. attribute:: CREATED
:annotation: = 201
Inform that the room was created during the join operation.
.. attribute:: REMOVED_BANNED
:annotation: = 301
Inform that the user was banned from the room.
.. attribute:: NICKNAME_CHANGE
:annotation: = 303
Inform about new nickname.
.. attribute:: REMOVED_KICKED
:annotation: = 307
Inform that the occupant was kicked.
.. attribute:: REMOVED_AFFILIATION_CHANGE
:annotation: = 321
Inform that the occupant was removed from the room due to a change in
affiliation.
.. attribute:: REMOVED_NONMEMBER_IN_MEMBERS_ONLY
:annotation: = 322
Inform that the occupant was removed from the room because the room was
changed to members-only and the occupant was not a member.
.. attribute:: REMOVED_SERVICE_SHUTDOWN
:annotation: = 332
Inform that the occupant is being removed because the MUC service is
being shut down.
.. attribute:: REMOVED_ERROR
:annotation: = 333
Inform that the occupant is being removed because there was an error
while communicating with them or their server.
"""
NON_ANONYMOUS = 100
AFFILIATION_CHANGE = 101
SHOWING_UNAVAILABLE = 102
NOT_SHOWING_UNAVAILABLE = 103
CONFIG_NON_PRIVACY_RELATED = 104
SELF = 110
CONFIG_ROOM_LOGGING = 170
CONFIG_NO_ROOM_LOGGING = 171
CONFIG_NON_ANONYMOUS = 172
CONFIG_SEMI_ANONYMOUS = 173
CREATED = 201
REMOVED_BANNED = 301
NICKNAME_CHANGE = 303
REMOVED_KICKED = 307
REMOVED_AFFILIATION_CHANGE = 321
REMOVED_NONMEMBER_IN_MEMBERS_ONLY = 322
REMOVED_SERVICE_SHUTDOWN = 332
REMOVED_ERROR = 333
class History(xso.XSO):
TAG = (namespaces.xep0045_muc, "history")
maxchars = xso.Attr(
"maxchars",
type_=xso.Integer(),
default=None,
)
maxstanzas = xso.Attr(
"maxstanzas",
type_=xso.Integer(),
default=None,
)
seconds = xso.Attr(
"seconds",
type_=xso.Integer(),
default=None,
)
since = xso.Attr(
"since",
type_=xso.DateTime(),
default=None,
)
def __init__(self, *,
maxchars=None, maxstanzas=None, seconds=None, since=None):
super().__init__()
self.maxchars = maxchars
self.maxstanzas = maxstanzas
self.seconds = seconds
self.since = since
class GenericExt(xso.XSO):
TAG = (namespaces.xep0045_muc, "x")
history = xso.Child([History])
password = xso.ChildText(
(namespaces.xep0045_muc, "password"),
default=None
)
aioxmpp.stanza.Presence.xep0045_muc = xso.Child([
GenericExt
])
aioxmpp.stanza.Message.xep0045_muc = xso.Child([
GenericExt
])
class Status(xso.XSO):
TAG = (namespaces.xep0045_muc_user, "status")
code = xso.Attr(
"code",
type_=xso.EnumCDataType(
StatusCode,
xso.Integer(),
allow_coerce=True,
pass_unknown=True,
)
)
def __init__(self, code):
super().__init__()
self.code = code
class StatusCodeList(xso.AbstractElementType):
def unpack(self, item):
return item.code
def pack(self, code):
item = Status(code)
return item
def get_xso_types(self):
return [Status]
class DestroyNotification(xso.XSO):
TAG = (namespaces.xep0045_muc_user, "destroy")
reason = xso.ChildText(
(namespaces.xep0045_muc_user, "reason"),
default=None
)
jid = xso.Attr(
"jid",
type_=xso.JID(),
default=None
)
class Decline(xso.XSO):
TAG = (namespaces.xep0045_muc_user, "decline")
from_ = xso.Attr(
"from",
type_=xso.JID(),
default=None
)
to = xso.Attr(
"to",
type_=xso.JID(),
default=None
)
reason = xso.ChildText(
(namespaces.xep0045_muc_user, "reason"),
default=None
)
class Invite(xso.XSO):
TAG = (namespaces.xep0045_muc_user, "invite")
from_ = xso.Attr(
"from",
type_=xso.JID(),
default=None
)
to = xso.Attr(
"to",
type_=xso.JID(),
default=None
)
reason = xso.ChildText(
(namespaces.xep0045_muc_user, "reason"),
default=None
)
password = xso.ChildText(
(namespaces.xep0045_muc_user, "password"),
default=None
)
class ActorBase(xso.XSO):
jid = xso.Attr(
"jid",
type_=xso.JID(),
default=None,
)
nick = xso.Attr(
"nick",
type_=xso.String(aioxmpp.stringprep.resourceprep),
default=None
)
class ItemBase(xso.XSO):
affiliation = xso.Attr(
"affiliation",
validator=xso.RestrictToSet({
"admin",
"member",
"none",
"outcast",
"owner",
None,
}),
validate=xso.ValidateMode.ALWAYS,
default=None,
)
jid = xso.Attr(
"jid",
type_=xso.JID(),
default=None,
)
nick = xso.Attr(
"nick",
type_=xso.String(aioxmpp.stringprep.resourceprep),
default=None
)
role = xso.Attr(
"role",
validator=xso.RestrictToSet({
"moderator",
"none",
"participant",
"visitor",
None,
}),
validate=xso.ValidateMode.ALWAYS,
default=None,
)
def __init__(self,
affiliation=None,
jid=None,
nick=None,
role=None,
reason=None):
super().__init__()
self.affiliation = affiliation
self.jid = jid
self.nick = nick
self.role = role
self.reason = reason
@property
def bare_jid(self):
"""
Return the bare jid of the item or :data:`None` if no JID is
given.
Use this to access the jid unless you really want to know the
resource. Usually the information given by the resource is
meaningless (the resource is randomly picked by the server).
"""
if self.jid:
return self.jid.bare()
else:
return None
class UserActor(ActorBase):
TAG = (namespaces.xep0045_muc_user, "actor")
class Continue(xso.XSO):
TAG = (namespaces.xep0045_muc_user, "continue")
thread = xso.Attr(
"thread",
type_=aioxmpp.stanza.Thread.identifier.type_,
default=None,
)
class UserItem(ItemBase):
TAG = (namespaces.xep0045_muc_user, "item")
actor = xso.Child([UserActor])
continue_ = xso.Child([Continue])
reason = xso.ChildText(
(namespaces.xep0045_muc_user, "reason"),
default=None
)
class UserExt(xso.XSO):
TAG = (namespaces.xep0045_muc_user, "x")
status_codes = xso.ChildValueList(
StatusCodeList(),
container_type=set
)
destroy = xso.Child([DestroyNotification])
decline = xso.Child([Decline])
invites = xso.ChildList([Invite])
items = xso.ChildList([UserItem])
password = xso.ChildText(
(namespaces.xep0045_muc_user, "password"),
default=None
)
def __init__(self,
status_codes=[],
destroy=None,
decline=None,
invites=[],
items=[],
password=None):
super().__init__()
self.status_codes.update(status_codes)
self.destroy = destroy
self.decline = decline
self.invites.extend(invites)
self.items.extend(items)
self.password = password
aioxmpp.stanza.Presence.xep0045_muc_user = xso.Child([
UserExt
])
aioxmpp.stanza.Message.xep0045_muc_user = xso.Child([
UserExt
])
class AdminActor(ActorBase):
TAG = (namespaces.xep0045_muc_admin, "actor")
class AdminItem(ItemBase):
TAG = (namespaces.xep0045_muc_admin, "item")
actor = xso.Child([AdminActor])
continue_ = xso.Child([Continue])
reason = xso.ChildText(
(namespaces.xep0045_muc_admin, "reason"),
default=None
)
@aioxmpp.stanza.IQ.as_payload_class
class AdminQuery(xso.XSO):
TAG = (namespaces.xep0045_muc_admin, "query")
items = xso.ChildList([AdminItem])
def __init__(self, *, items=[]):
super().__init__()
self.items[:] = items
class DestroyRequest(xso.XSO):
TAG = (namespaces.xep0045_muc_owner, "destroy")
reason = xso.ChildText(
(namespaces.xep0045_muc_owner, "reason"),
default=None
)
password = xso.ChildText(
(namespaces.xep0045_muc_owner, "password"),
default=None
)
jid = xso.Attr(
"jid",
type_=xso.JID(),
default=None
)
@aioxmpp.stanza.IQ.as_payload_class
class OwnerQuery(xso.XSO):
TAG = (namespaces.xep0045_muc_owner, "query")
destroy = xso.Child([DestroyRequest])
form = xso.Child([aioxmpp.forms.Data])
def __init__(self, *, form=None, destroy=None):
super().__init__()
self.form = form
self.destroy = destroy
class DirectInvite(xso.XSO):
TAG = namespaces.xep0249_conference, "x"
# JEP-0045 v1.19 §6.7 allowed a mediated(!) invitation to contain a
# (what is now) DirectInvite payload where the reason is included as
# text (and not as attribute).
#
# Some servers still emit this for compatibility. We ignore that.
_ = xso.Text(default=None)
jid = xso.Attr(
"jid",
type_=xso.JID(),
)
reason = xso.Attr(
"reason",
default=None,
)
password = xso.Attr(
"password",
default=None,
)
continue_ = xso.Attr(
"continue",
type_=xso.Bool(),
default=False,
)
thread = xso.Attr(
"thread",
default=None,
)
def __init__(self, jid, *,
reason=None,
password=None,
continue_=False,
thread=None):
super().__init__()
self.jid = jid
self.reason = reason
self.password = password
self.continue_ = continue_
self.thread = thread
aioxmpp.Message.xep0249_direct_invite = xso.Child([DirectInvite])
class ConfigurationForm(aioxmpp.forms.Form):
"""
This is a :xep:`4` form template (see :mod:`aioxmpp.forms`) for MUC
configuration forms.
The attribute documentation is auto-generated from :xep:`45`; see there for
details on the semantics of each field.
.. versionadded:: 0.7
"""
FORM_TYPE = 'http://jabber.org/protocol/muc#roomconfig'
maxhistoryfetch = aioxmpp.forms.TextSingle(
var='muc#maxhistoryfetch',
label='Maximum Number of History Messages Returned by Room'
)
allowpm = aioxmpp.forms.ListSingle(
var='muc#roomconfig_allowpm',
label='Roles that May Send Private Messages'
)
allowinvites = aioxmpp.forms.Boolean(
var='muc#roomconfig_allowinvites',
label='Whether to Allow Occupants to Invite Others'
)
changesubject = aioxmpp.forms.Boolean(
var='muc#roomconfig_changesubject',
label='Whether to Allow Occupants to Change Subject'
)
enablelogging = aioxmpp.forms.Boolean(
var='muc#roomconfig_enablelogging',
label='Whether to Enable Public Logging of Room Conversations'
)
getmemberlist = aioxmpp.forms.ListMulti(
var='muc#roomconfig_getmemberlist',
label='Roles and Affiliations that May Retrieve Member List'
)
lang = aioxmpp.forms.TextSingle(
var='muc#roomconfig_lang',
label='Natural Language for Room Discussions'
)
pubsub = aioxmpp.forms.TextSingle(
var='muc#roomconfig_pubsub',
label='XMPP URI of Associated Publish-Subscribe Node'
)
maxusers = aioxmpp.forms.ListSingle(
var='muc#roomconfig_maxusers',
label='Maximum Number of Room Occupants'
)
membersonly = aioxmpp.forms.Boolean(
var='muc#roomconfig_membersonly',
label='Whether to Make Room Members-Only'
)
moderatedroom = aioxmpp.forms.Boolean(
var='muc#roomconfig_moderatedroom',
label='Whether to Make Room Moderated'
)
passwordprotectedroom = aioxmpp.forms.Boolean(
var='muc#roomconfig_passwordprotectedroom',
label='Whether a Password is Required to Enter'
)
persistentroom = aioxmpp.forms.Boolean(
var='muc#roomconfig_persistentroom',
label='Whether to Make Room Persistent'
)
presencebroadcast = aioxmpp.forms.ListMulti(
var='muc#roomconfig_presencebroadcast',
label='Roles for which Presence is Broadcasted'
)
publicroom = aioxmpp.forms.Boolean(
var='muc#roomconfig_publicroom',
label='Whether to Allow Public Searching for Room'
)
roomadmins = aioxmpp.forms.JIDMulti(
var='muc#roomconfig_roomadmins',
label='Full List of Room Admins'
)
roomdesc = aioxmpp.forms.TextSingle(
var='muc#roomconfig_roomdesc',
label='Short Description of Room'
)
roomname = aioxmpp.forms.TextSingle(
var='muc#roomconfig_roomname',
label='Natural-Language Room Name'
)
roomowners = aioxmpp.forms.JIDMulti(
var='muc#roomconfig_roomowners',
label='Full List of Room Owners'
)
roomsecret = aioxmpp.forms.TextPrivate(
var='muc#roomconfig_roomsecret',
label='The Room Password'
)
whois = aioxmpp.forms.ListSingle(
var='muc#roomconfig_whois',
label='Affiliations that May Discover Real JIDs of Occupants'
)
class InfoForm(aioxmpp.forms.Form):
FORM_TYPE = 'http://jabber.org/protocol/muc#roominfo'
maxhistoryfetch = aioxmpp.forms.TextSingle(
var='muc#maxhistoryfetch',
label='Maximum Number of History Messages Returned by Room'
)
contactjid = aioxmpp.forms.JIDMulti(
var='muc#roominfo_contactjid',
label='Contact Addresses (normally, room owner or owners)'
)
description = aioxmpp.forms.TextSingle(
var='muc#roominfo_description',
label='Short Description of Room'
)
lang = aioxmpp.forms.TextSingle(
var='muc#roominfo_lang',
label='Natural Language for Room Discussions'
)
ldapgroup = aioxmpp.forms.TextSingle(
var='muc#roominfo_ldapgroup',
label='An associated LDAP group that defines room membership; this '
'should be an LDAP Distinguished Name according to an '
'implementation-specific or deployment-specific definition of a group.'
)
logs = aioxmpp.forms.TextSingle(
var='muc#roominfo_logs',
label='URL for Archived Discussion Logs'
)
occupants = aioxmpp.forms.TextSingle(
var='muc#roominfo_occupants',
label='Current Number of Occupants in Room'
)
subject = aioxmpp.forms.TextSingle(
var='muc#roominfo_subject',
label='Current Discussion Topic'
)
subjectmod = aioxmpp.forms.Boolean(
var='muc#roominfo_subjectmod',
label='The room subject can be modified by participants'
)
class VoiceRequestForm(aioxmpp.forms.Form):
FORM_TYPE = 'http://jabber.org/protocol/muc#request'
role = aioxmpp.forms.ListSingle(
var='muc#role',
label='Requested role'
)
jid = aioxmpp.forms.JIDSingle(
var='muc#jid',
label='User ID'
)
roomnick = aioxmpp.forms.TextSingle(
var='muc#roomnick',
label='Room Nickname'
)
request_allow = aioxmpp.forms.Boolean(
var='muc#request_allow',
label='Whether to grant voice'
)