v1.3.5
This commit is contained in:
@@ -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 user’s 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 they’re 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
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -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 won’t 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'
|
||||
)
|
||||
Reference in New Issue
Block a user