diff --git a/slixmpp/exceptions.py b/slixmpp/exceptions.py index 0486666e..5ec6e7e1 100644 --- a/slixmpp/exceptions.py +++ b/slixmpp/exceptions.py @@ -101,3 +101,17 @@ class IqError(XMPPError): #: The :class:`~slixmpp.stanza.iq.Iq` error result stanza. self.iq = iq + + +class PresenceError(XMPPError): + """ + An exception raised in specific circumstances for presences + of type 'error' received. + """ + def __init__(self, pres): + super().__init__( + condition=pres['error']['condition'], + text=pres['error']['text'], + etype=pres['error']['type'], + ) + self.presence = pres diff --git a/slixmpp/plugins/xep_0045/muc.py b/slixmpp/plugins/xep_0045/muc.py index 905e0f49..e156ded1 100644 --- a/slixmpp/plugins/xep_0045/muc.py +++ b/slixmpp/plugins/xep_0045/muc.py @@ -8,8 +8,11 @@ """ from __future__ import with_statement +import asyncio import logging +from datetime import datetime from typing import ( + Dict, List, Tuple, Optional, @@ -27,7 +30,7 @@ from slixmpp.xmlstream.handler.callback import Callback from slixmpp.xmlstream.matcher.xpath import MatchXPath from slixmpp.xmlstream.matcher.stanzapath import StanzaPath from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask -from slixmpp.exceptions import IqError, IqTimeout +from slixmpp.exceptions import IqError, IqTimeout, PresenceError from slixmpp.plugins.xep_0045 import stanza from slixmpp.plugins.xep_0045.stanza import ( @@ -91,6 +94,9 @@ class XEP_0045(BasePlugin): StanzaPath("presence/muc"), self.handle_groupchat_presence, )) + # is only used in + # presence when joining on the client side, and for errors on + # the server side. if self.xmpp.is_component: self.xmpp.register_handler( Callback( @@ -98,6 +104,13 @@ class XEP_0045(BasePlugin): StanzaPath("presence/muc_join"), self.handle_groupchat_join, )) + self.xmpp.register_handler( + Callback( + "MUCPresenceError", + StanzaPath("presence@type=error/muc_join"), + self._handle_presence_error, + ) + ) self.xmpp.register_handler( Callback( @@ -184,12 +197,18 @@ class XEP_0045(BasePlugin): self.rooms[entry['room']][entry['nick']] = entry log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry) self.xmpp.event("groupchat_presence", pr) + if 110 in pr['muc']['status_codes']: + self.xmpp.event("muc::%s::self-presence" % entry['room'], pr) self.xmpp.event("muc::%s::presence" % entry['room'], pr) if got_offline: self.xmpp.event("muc::%s::got_offline" % entry['room'], pr) if got_online: self.xmpp.event("muc::%s::got_online" % entry['room'], pr) + def _handle_presence_error(self, pr: Presence): + """Generate MUC presence error events""" + self.xmpp.event("muc::%s::presence-error" % pr['from'].bare, pr) + def handle_groupchat_presence(self, pr: Presence): """ Handle a presence in a muc.""" if self.xmpp.is_component: @@ -238,6 +257,70 @@ class XEP_0045(BasePlugin): return nick return None + async def join_muc_wait(self, room: JID, nick: str, *, + password: Optional[str] = None, + maxchars: Optional[int] = None, + maxstanzas: Optional[int] = None, + seconds: Optional[int] = None, + since: Optional[datetime] = None, + presence_options: Optional[Dict[str, str]] = None, + timeout: int = 30) -> Presence: + """ + Try to join a MUC and block until we are joined or get an error. + + Only one of {maxchars, maxstanzas, seconds, since} will be used, in + that order. + + :param password: The optional room password. + :param maxchars: Max number of characters to return from history. + :param maxstanzas: Max number of stanzas to return from history. + :param seconds: Fetch history until that many seconds in the past. + :param since: Fetch history since that timestamp. + :raises: A slixmpp.exceptions.PresenceError if the MUC returns a + presence error. + :raises: An asyncio.TimeoutError if there is neither success nor + presence error when the timeout is reached. + :return: Our own presence + """ + if presence_options is None: + presence_options = {} + stanza = self.xmpp.make_presence( + pto="%s/%s" % (room, nick), + **presence_options + ) + stanza.enable('muc_join') + if password is not None: + stanza['muc_join']['password'] = password + if maxchars is not None: + stanza['muc_join']['history']['maxchars'] = str(maxchars) + elif maxstanzas is not None: + stanza['muc_join']['history']['maxstanzas'] = str(maxstanzas) + elif seconds is not None: + stanza['muc_join']['history']['seconds'] = str(seconds) + elif since is not None: + fmt = self.xmpp.plugin['xep_0082'].format_datetime(since) + stanza['muc_join']['history']['since'] = fmt + self.rooms[room] = {} + self.our_nicks[room] = nick + stanza.send() + + future = asyncio.Future() + context1 = self.xmpp.event_handler("muc::%s::self-presence" % room, future.set_result) + context2 = self.xmpp.event_handler("muc::%s::presence-error" % room, future.set_result) + with context1, context2: + done, pending = await asyncio.wait( + [future], + timeout=timeout, + ) + if pending: + raise asyncio.TimeoutError() + pres = await future + if pres['type'] == 'error': + raise PresenceError(pres) + # update known nick in case it has changed + self.our_nicks[room] = pres['from'].resource + return pres + def join_muc(self, room: JID, nick: str, maxhistory="0", password='', pstatus='', pshow='', pfrom=''): """ Join the specified room, requesting 'maxhistory' lines of history. diff --git a/slixmpp/xmlstream/xmlstream.py b/slixmpp/xmlstream/xmlstream.py index 5074aa8c..b80c55d3 100644 --- a/slixmpp/xmlstream/xmlstream.py +++ b/slixmpp/xmlstream/xmlstream.py @@ -30,7 +30,7 @@ import weakref import uuid from asyncio import iscoroutinefunction, wait, Future - +from contextlib import contextmanager import xml.etree.ElementTree as ET from slixmpp.xmlstream.asyncio import asyncio @@ -1208,3 +1208,16 @@ class XMLStream(asyncio.BaseProtocol): disposable=True, ) return await asyncio.wait_for(fut, timeout, loop=self.loop) + + @contextmanager + def event_handler(self, event: str, handler: Callable): + """ + Context manager that adds then removes an event handler. + """ + self.add_event_handler(event, handler) + try: + yield + except Exception as exc: + raise + finally: + self.del_event_handler(event, handler)