class NodeBuilder:
    """
    Builds a Node class minidom from data parsed to it. This class used for two
    purposes:

      1. Creation an XML Node from a textual representation. F.e. reading a
         config file. See an XML2Node method.
      2. Handling an incoming XML stream. This is done by mangling the
         __dispatch_depth parameter and redefining the dispatch method.

    You do not need to use this class directly if you do not designing your own
    XML handler
    """

    _parser: Any
    Parse: Callable[[str, bool], None]
    __depth: int
    __last_depth: int
    __max_depth: int
    _dispatch_depth: int
    _document_attrs: Optional[Attrs]
    _document_nsp: Optional[Dict[str, str]]
    _mini_dom: Optional[Node]
    last_is_data: bool
    _ptr: Optional[Node]
    data_buffer: Optional[List[str]]
    streamError: str
    _is_stream: bool

    def __init__(self,
                 data: Optional[str] = None,
                 initial_node: Optional[Node] = None,
                 dispatch_depth: int = 1,
                 finished: bool = True) -> None:
        """
        Take two optional parameters: "data" and "initial_node"

        By default class initialised with empty Node class instance. Though, if
        "initial_node" is provided it used as "starting point". You can think
        about it as of "node upgrade". "data" (if provided) feeded to parser
        immidiatedly after instance init.
        """
        self._parser = xml.parsers.expat.ParserCreate()
        self._parser.UseForeignDTD(False)
        self._parser.StartElementHandler = self.starttag
        self._parser.EndElementHandler = self.endtag
        self._parser.StartNamespaceDeclHandler = self.handle_namespace_start
        self._parser.CharacterDataHandler = self.handle_cdata
        self._parser.StartDoctypeDeclHandler = self.handle_invalid_xmpp_element
        self._parser.EntityDeclHandler = self.handle_invalid_xmpp_element
        self._parser.CommentHandler = self.handle_invalid_xmpp_element
        self._parser.ExternalEntityRefHandler = self.handle_invalid_xmpp_element
        self._parser.AttlistDeclHandler = self.handle_invalid_xmpp_element
        self._parser.ProcessingInstructionHandler = \
            self.handle_invalid_xmpp_element
        self._parser.buffer_text = True
        self.Parse = self._parser.Parse

        self.__depth = 0
        self.__last_depth = 0
        self.__max_depth = 0
        self._dispatch_depth = dispatch_depth
        self._document_attrs = None
        self._document_nsp = None
        self._mini_dom = initial_node
        self.last_is_data = True
        self._ptr = None
        self.data_buffer = None
        self.streamError = ''
        self._is_stream = not finished
        if data:
            self._parser.Parse(data, finished)

    def check_data_buffer(self) -> None:
        if self.data_buffer:
            self._ptr.data.append(''.join(self.data_buffer))
            del self.data_buffer[:]
            self.data_buffer = None

    def destroy(self) -> None:
        """
        Method used to allow class instance to be garbage-collected
        """
        self.check_data_buffer()
        self._parser.StartElementHandler = None
        self._parser.EndElementHandler = None
        self._parser.CharacterDataHandler = None
        self._parser.StartNamespaceDeclHandler = None

    def starttag(self, tag: str, attrs: Attrs) -> None:
        """
        XML Parser callback. Used internally
        """
        self.check_data_buffer()
        self._inc_depth()
        log.debug("STARTTAG.. DEPTH -> %i , tag -> %s, attrs -> %s",
                  self.__depth, tag, attrs)
        if self.__depth == self._dispatch_depth:
            if not self._mini_dom:
                self._mini_dom = Node(tag=tag,
                                      attrs=attrs,
                                      nsp=self._document_nsp,
                                      node_built=True)
            else:
                Node.__init__(self._mini_dom,
                              tag=tag,
                              attrs=attrs,
                              nsp=self._document_nsp,
                              node_built=True)
            self._ptr = self._mini_dom
        elif self.__depth > self._dispatch_depth:
            self._ptr.kids.append(Node(tag=tag,
                                       parent=self._ptr,
                                       attrs=attrs,
                                       node_built=True))
            self._ptr = self._ptr.kids[-1]
        if self.__depth == 1:
            self._document_attrs = {}
            self._document_nsp = {}
            nsp, name = (['']+tag.split(':'))[-2:]
            for attr, val in attrs.items():
                if attr == 'xmlns':
                    self._document_nsp[''] = val
                elif attr.startswith('xmlns:'):
                    self._document_nsp[attr[6:]] = val
                else:
                    self._document_attrs[attr] = val
            ns = self._document_nsp.get(
                nsp, 'http://www.gajim.org/xmlns/undeclared-root')
            try:
                header = Node(tag=tag,
                              attrs=attrs,
                              nsp=self._document_nsp, node_built=True)
                self.dispatch(header)
                self._check_stream_start(ns, name)
            except ValueError as error:
                self._document_attrs = None
                raise ValueError(str(error))
        if not self.last_is_data and self._ptr.parent:
            self._ptr.parent.data.append('')
        self.last_is_data = False

    def _check_stream_start(self, ns: str, tag: str) -> None:
        if self._is_stream:
            if ns != 'http://etherx.jabber.org/streams' or tag != 'stream':
                raise ValueError('Incorrect stream start: (%s,%s). Terminating.'
                                 % (tag, ns))
        else:
            self.stream_header_received()

    def endtag(self, tag: str) -> None:
        """
        XML Parser callback. Used internally
        """
        log.debug("DEPTH -> %i , tag -> %s", self.__depth, tag)
        self.check_data_buffer()
        if self.__depth == self._dispatch_depth:
            if self._mini_dom.getName() == 'error':
                children = self._mini_dom.getChildren()
                if children:
                    self.streamError = children[0].getName()
                else:
                    self.streamError = self._mini_dom.getData()
            self.dispatch(self._mini_dom)
        elif self.__depth > self._dispatch_depth:
            self._ptr = self._ptr.parent
        else:
            log.debug("Got higher than dispatch level. Stream terminated?")
        self._dec_depth()
        self.last_is_data = False
        if self.__depth == 0:
            self.stream_footer_received()

    def handle_cdata(self, data: str) -> None:
        if self.last_is_data:
            if self.data_buffer:
                self.data_buffer.append(data)
        elif self._ptr:
            self.data_buffer = [data]
            self.last_is_data = True

    @staticmethod
    def handle_invalid_xmpp_element(*args: Any) -> None:
        raise ExpatError('Found invalid xmpp stream element: %s' % str(args))

    def handle_namespace_start(self, _prefix: str, _uri: str) -> None:
        """
        XML Parser callback. Used internally
        """
        self.check_data_buffer()

    def getDom(self) -> Optional[Node]:
        """
        Return just built Node
        """
        self.check_data_buffer()
        return self._mini_dom

    def dispatch(self, stanza: Any) -> None:
        """
        Get called when the NodeBuilder reaches some level of depth on it's way
        up with the built node as argument. Can be redefined to convert incoming
        XML stanzas to program events
        """

    def stream_header_received(self) -> None:
        """
        Method called when stream just opened
        """
        self.check_data_buffer()

    def stream_footer_received(self) -> None:
        """
        Method called when stream just closed
        """
        self.check_data_buffer()

    def has_received_endtag(self, level: int = 0) -> bool:
        """
        Return True if at least one end tag was seen (at level)
        """
        return self.__depth <= level < self.__max_depth

    def _inc_depth(self) -> None:
        self.__last_depth = self.__depth
        self.__depth += 1
        self.__max_depth = max(self.__depth, self.__max_depth)

    def _dec_depth(self) -> None:
        self.__last_depth = self.__depth
        self.__depth -= 1 class Node:
    """
    Node class describes syntax of separate XML Node. It have a constructor that
    permits node creation from set of "namespace name", attributes and payload
    of text strings and other nodes. It does not natively support building node
    from text string and uses NodeBuilder class for that purpose. After
    creation node can be mangled in many ways so it can be completely changed.
    Also node can be serialised into string in one of two modes: default (where
    the textual representation of node describes it exactly) and "fancy" - with
    whitespace added to make indentation and thus make result more readable by
    human.

    Node class have attribute FORCE_NODE_RECREATION that is defaults to False
    thus enabling fast node replication from the some other node. The drawback
    of the fast way is that new node shares some info with the "original" node
    that is changing the one node may influence the other. Though it is rarely
    needed (in xmpppy it is never needed at all since I'm usually never using
    original node after replication (and using replication only to move upwards
    on the classes tree).
    """

    name: str
    namespace: str
    attrs: Attrs
    data: List[str]
    kids: List[Union[Node, str]]
    parent: Optional[Node]
    nsd: Dict[str, str]
    nsp_cache: Dict[Any, Any]

    FORCE_NODE_RECREATION = False

    def __init__(
            self,
            tag: Optional[str] = None,
            attrs: Optional[Attrs] = None,
            payload: Optional[Union[Node, str, List[Union[Node, str]]]] = None,
            parent: Optional[Node] = None,
            nsp: Optional[Dict[Any, Any]] = None,
            node_built: bool = False,
            node: Optional[Union[Node, Any]] = None) -> None:
        """
        Takes "tag" argument as the name of node (prepended by namespace, if
        needed and separated from it by a space), attrs dictionary as the set of
        arguments, payload list as the set of textual strings and child nodes
        that this node carries within itself and "parent" argument that is
        another node that this one will be the child of. Also the __init__ can
        be provided with "node" argument that is either a text string containing
        exactly one node or another Node instance to begin with. If both "node"
        and other arguments is provided then the node initially created as
        replica of "node" provided and then modified to be compliant with other
        arguments.
        """
        if node:
            if self.FORCE_NODE_RECREATION and isinstance(node, Node):
                node = str(node)
            if not isinstance(node, Node):
                node = NodeBuilder(node, self)
                node_built = True
            else:
                self.name = node.name
                self.namespace = node.namespace
                self.attrs = {}
                self.data = []
                self.kids = []
                self.parent = node.parent
                self.nsd = {}
                for key in node.attrs.keys():
                    self.attrs[key] = node.attrs[key]
                for data in node.data:
                    self.data.append(data)
                for kid in node.kids:
                    self.kids.append(kid)
                for key, value in node.nsd.items():
                    self.nsd[key] = value
        else:
            self.name = 'tag'
            self.namespace = ''
            self.attrs = {}
            self.data = []
            self.kids = []
            self.parent = None
            self.nsd = {}
        if parent:
            self.parent = parent
        self.nsp_cache = {}
        if nsp:
            for key, value in nsp.items():
                self.nsp_cache[key] = value

        if attrs is not None:
            for attr, val in attrs.items():
                if attr == 'xmlns':
                    self.nsd[''] = val
                elif attr.startswith('xmlns:'):
                    self.nsd[attr[6:]] = val
                self.attrs[attr] = attrs[attr]

        if tag:
            if node_built:
                pfx, self.name = (['']+tag.split(':'))[-2:]
                self.namespace = self.lookup_nsp(pfx)
            else:
                if ' ' in tag:
                    self.namespace, self.name = tag.split()
                else:
                    self.name = tag
        if payload is not None:
            if not isinstance(payload, list):
                payload = [payload]
            for i in payload:
                if isinstance(i, Node):
                    self.addChild(node=i)
                else:
                    self.data.append(str(i))

    def lookup_nsp(self, pfx: str = '') -> str:
        ns = self.nsd.get(pfx, None)
        if ns is None:
            ns = self.nsp_cache.get(pfx, None)
        if ns is None:
            if self.parent:
                ns = self.parent.lookup_nsp(pfx)
                self.nsp_cache[pfx] = ns
            else:
                return 'http://www.gajim.org/xmlns/undeclared'
        return ns

    def __str__(self, fancy: int = 0) -> str:
        """
        Method used to dump node into textual representation. If "fancy"
        argument is set to True produces indented output for readability
        """
        s = (fancy-1) * 2 * ' ' + "<" + self.name
        if self.namespace:
            if not self.parent or self.parent.namespace!=self.namespace:
                if 'xmlns' not in self.attrs:
                    s += ' xmlns="%s"' % self.namespace
        for key in self.attrs.keys():
            val = str(self.attrs[key])
            s += ' %s="%s"' % (key, XMLescape(val))

        s += ">"
        cnt = 0
        if self.kids:
            if fancy:
                s += "\n"
            for a in self.kids:
                if not fancy and (len(self.data)-1) >= cnt:
                    s += XMLescape(self.data[cnt])
                elif (len(self.data)-1) >= cnt:
                    s += XMLescape(self.data[cnt].strip())
                if isinstance(a, str):
                    s += a.__str__()
                else:
                    s += a.__str__(fancy and fancy+1)
                cnt += 1
        if not fancy and (len(self.data)-1) >= cnt:
            s += XMLescape(self.data[cnt])
        elif (len(self.data)-1) >= cnt:
            s += XMLescape(self.data[cnt].strip())
        if not self.kids and s.endswith('>'):
            s = s[:-1] + ' />'
            if fancy:
                s += "\n"
        else:
            if fancy and not self.data:
                s += (fancy-1) * 2 * ' '
            s += "</" + self.name + ">"
            if fancy:
                s += "\n"
        return s

    def addChild(self,
                 name: Optional[str] = None,
                 attrs: Optional[Attrs] = None,
                 payload: Optional[List[Any]] = None,
                 namespace: Optional[str] = None,
                 node: Optional[Node] = None) -> Node:
        """
        If "node" argument is provided, adds it as child node. Else creates new
        node from the other arguments' values and adds it as well
        """
        if payload is None:
            payload = []

        if attrs is None:
            attrs = {}
        elif 'xmlns' in attrs:
            raise AttributeError("Use namespace=x instead of attrs={'xmlns':x}")
        if node:
            newnode=node
            node.parent = self
        else: newnode=Node(tag=name, parent=self, attrs=attrs, payload=payload)
        if namespace:
            newnode.setNamespace(namespace)
        self.kids.append(newnode)
        return newnode

    def addData(self, data: Any) -> None:
        """
        Add some CDATA to node
        """
        self.data.append(str(data))

    def clearData(self) -> None:
        """
        Remove all CDATA from the node
        """
        self.data = []

    def delAttr(self, key: str) -> None:
        """
        Delete an attribute "key"
        """
        del self.attrs[key]

    def delChild(self,
                 node: Union[Node, str],
                 attrs: Optional[Attrs] = None) -> Optional[Node]:
        """
        Delete the "node" from the node's childs list, if "node" is an instance.
        Else delete the first node that have specified name and (optionally)
        attributes
        """
        if not isinstance(node, Node):
            node = self.getTag(node, attrs)

        self.kids.remove(node)
        return node

    def getAttrs(self, copy: bool = False) -> Attrs:
        """
        Return all node's attributes as dictionary
        """
        if copy:
            return deepcopy(self.attrs)
        return self.attrs

    def getAttr(self, key: str) -> Optional[str]:
        """
        Return value of specified attribute
        """
        return self.attrs.get(key)

    def getChildren(self) -> List[Union[Node, str]]:
        """
        Return all node's child nodes as list
        """
        return self.kids

    def getData(self) -> str:
        """
        Return all node CDATA as string (concatenated)
        """
        return ''.join(self.data)

    def getName(self) -> str:
        """
        Return the name of node
        """
        return self.name

    def getNamespace(self) -> str:
        """
        Return the namespace of node
        """
        return self.namespace

    def getParent(self) -> Optional[Node]:
        """
        Returns the parent of node (if present)
        """
        return self.parent

    def getTag(self,
               name: str,
               attrs: Optional[Attrs] = None,
               namespace: Optional[str] = None) -> Optional[Node]:
        """
        Filter all child nodes using specified arguments as filter. Return the
        first found or None if not found
        """
        tags = self.getTags(name, attrs, namespace)
        if not tags:
            return None
        return tags[0]

    def getTagAttr(self,
                   tag: str,
                   attr: str,
                   namespace: Optional[str] = None) -> Optional[str]:
        """
        Return attribute value of the child with specified name (or None if no
        such attribute)
        """
        node = self.getTag(tag, namespace=namespace)
        if node is None:
            return None
        return node.getAttr(attr)

    def getTagData(self, tag: str) -> Optional[str]:
        """
        Return cocatenated CDATA of the child with specified name
        """
        node = self.getTag(tag)
        if node is None:
            return None
        return node.getData()

    def getTags(self,
                name: str,
                attrs: Optional[Attrs] = None,
                namespace: Optional[str] = None) -> List[Node]:
        """
        Filter all child nodes using specified arguments as filter. Returns the
        list of nodes found
        """
        nodes: List[Node] = []
        for node in self.kids:
            if namespace and namespace != node.getNamespace():
                continue
            if node.getName() == name:
                if attrs is None:
                    attrs = {}
                for key in attrs.keys():
                    if key not in node.attrs or node.attrs[key] != attrs[key]:
                        break
                else:
                    nodes.append(node)
        return nodes

    def iterTags(self,
                 name: str,
                 attrs: Optional[Attrs] = None,
                 namespace: Optional[str] = None) -> Iterator[Node]:
        """
        Iterate over all children using specified arguments as filter
        """
        for node in self.kids:
            if namespace is not None and namespace != node.getNamespace():
                continue
            if node.getName() == name:
                if attrs is None:
                    attrs = {}
                for key in attrs.keys():
                    if key not in node.attrs or \
                            node.attrs[key]!=attrs[key]:
                        break
                else:
                    yield node

    def setAttr(self, key: str, val: str) -> None:
        """
        Set attribute "key" with the value "val"
        """
        self.attrs[key] = val

    def setData(self, data: Any) -> None:
        """
        Set node's CDATA to provided string. Resets all previous CDATA!
        """
        self.data = [str(data)]

    def setName(self, val: str) -> None:
        """
        Change the node name
        """
        self.name = val

    def setNamespace(self, namespace: str) -> None:
        """
        Changes the node namespace
        """
        self.namespace = namespace

    def setParent(self, node: Node) -> None:
        """
        Set node's parent to "node". WARNING: do not checks if the parent
        already present and not removes the node from the list of childs of
        previous parent
        """
        self.parent = node

    def setPayload(self,
                   payload: Union[List[Union[Node, str]], Node, str],
                   add: bool = False) -> None:
        """
        Set node payload according to the list specified. WARNING: completely
        replaces all node's previous content. If you wish just to add child or
        CDATA - use addData or addChild methods
        """
        if not isinstance(payload, list):
            payload = [payload]
        if add:
            self.kids += payload
        else:
            self.kids = payload

    def setTag(self,
               name: str,
               attrs: Optional[Attrs] = None,
               namespace: Optional[str] = None) -> Node:
        """
        Same as getTag but if the node with specified namespace/attributes not
        found, creates such node and returns it
        """
        nodes = self.getTags(name, attrs, namespace=namespace)
        if nodes:
            return nodes[0]
        return self.addChild(name, attrs, namespace=namespace)

    def setTagAttr(self,
                   tag: str,
                   attr: str,
                   val: str,
                   namespace: Optional[str] = None) -> None:
        """
        Create new node (if not already present) with name "tag" and set it's
        attribute "attr" to value "val"
        """
        try:
            self.getTag(tag, namespace=namespace).attrs[attr] = val
        except Exception:
            self.addChild(tag, namespace=namespace, attrs={attr: val})

    def setTagData(self,
                   tag: str,
                   val: str,
                   attrs: Optional[Attrs] = None) -> None:
        """
        Creates new node (if not already present) with name "tag" and
        (optionally) attributes "attrs" and sets it's CDATA to string "val"
        """
        try:
            self.getTag(tag, attrs).setData(str(val))
        except Exception:
            self.addChild(tag, attrs, payload = [str(val)])

    def getXmlLang(self) -> Optional[str]:
        lang = self.attrs.get('xml:lang')
        if lang is not None:
            return lang

        if self.parent is not None:
            return self.parent.getXmlLang()
        return None

    def has_attr(self, key: str) -> bool:
        """
        Check if node have attribute "key"
        """
        return key in self.attrs

    def __getitem__(self, item: str) -> Optional[str]:
        """
        Return node's attribute "item" value
        """
        return self.getAttr(item)

    def __setitem__(self, item: str, val: str) -> None:
        """
        Set node's attribute "item" value
        """
        self.setAttr(item, val)

    def __delitem__(self, item: str) -> None:
        """
        Delete node's attribute "item"
        """
        self.delAttr(item)

    def __contains__(self, item: str) -> bool:
        """
        Check if node has attribute "item"
        """
        return self.has_attr(item)

    def __getattr__(self, attr: str) -> Union['T', 'NT']:
        """
        Reduce memory usage caused by T/NT classes - use memory only when needed
        """
        if attr == 'T':
            self.T = T(self)
            return self.T
        if attr == 'NT':
            self.NT = NT(self)
            return self.NT
        raise AttributeError class _Namespaces:
    ACTIVITY: str = 'http://jabber.org/protocol/activity'
    ADDRESS: str = 'http://jabber.org/protocol/address'
    AGENTS: str = 'jabber:iq:agents'
    ATTENTION: str = 'urn:xmpp:attention:0'
    AUTH: str = 'jabber:iq:auth'
    AVATAR_METADATA: str = 'urn:xmpp:avatar:metadata'
    AVATAR_DATA: str = 'urn:xmpp:avatar:data'
    BIND: str = 'urn:ietf:params:xml:ns:xmpp-bind'
    BLOCKING: str = 'urn:xmpp:blocking'
    BOB: str = 'urn:xmpp:bob'
    BOOKMARKS: str = 'storage:bookmarks'
    BOOKMARKS_1: str = 'urn:xmpp:bookmarks:1'
    BOOKMARKS_COMPAT: str = 'urn:xmpp:bookmarks:1#compat'
    BOOKMARKS_COMPAT_PEP: str = 'urn:xmpp:bookmarks:1#compat-pep'
    BOOKMARK_CONVERSION: str = 'urn:xmpp:bookmarks-conversion:0'
    BROWSE: str = 'jabber:iq:browse'
    BYTESTREAM: str = 'http://jabber.org/protocol/bytestreams'
    CAPS: str = 'http://jabber.org/protocol/caps'
    CAPTCHA: str = 'urn:xmpp:captcha'
    CARBONS: str = 'urn:xmpp:carbons:2'
    CHATMARKERS: str = 'urn:xmpp:chat-markers:0'
    CHATSTATES: str = 'http://jabber.org/protocol/chatstates'
    CLIENT: str = 'jabber:client'
    COMMANDS: str = 'http://jabber.org/protocol/commands'
    CONFERENCE: str = 'jabber:x:conference'
    CORRECT: str = 'urn:xmpp:message-correct:0'
    DATA: str = 'jabber:x:data'
    DATA_LAYOUT: str = 'http://jabber.org/protocol/xdata-layout'
    DATA_MEDIA: str = 'urn:xmpp:media-element'
    DATA_VALIDATE: str = 'http://jabber.org/protocol/xdata-validate'
    DELAY: str = 'jabber:x:delay'
    DELAY2: str = 'urn:xmpp:delay'
    DELIMITER: str = 'roster:delimiter'
    DISCO: str = 'http://jabber.org/protocol/disco'
    DISCO_INFO: str = 'http://jabber.org/protocol/disco#info'
    DISCO_ITEMS: str = 'http://jabber.org/protocol/disco#items'
    DOMAIN_BASED_NAME: str = 'urn:xmpp:domain-based-name:1'
    EME: str = 'urn:xmpp:eme:0'
    ENCRYPTED: str = 'jabber:x:encrypted'
    FASTEN: str = 'urn:xmpp:fasten:0'
    FILE_METADATA: str = 'urn:xmpp:file:metadata:0'
    FORWARD: str = 'urn:xmpp:forward:0'
    FRAMING: str = 'urn:ietf:params:xml:ns:xmpp-framing'
    GATEWAY: str = 'jabber:iq:gateway'
    GEOLOC: str = 'http://jabber.org/protocol/geoloc'
    HASHES: str = 'urn:xmpp:hashes:1'
    HASHES_2: str = 'urn:xmpp:hashes:2'
    HASHES_BLAKE2B_256: str = 'urn:xmpp:hash-function-text-names:id-blake2b256'
    HASHES_BLAKE2B_512: str = 'urn:xmpp:hash-function-text-names:id-blake2b512'
    HASHES_MD5: str = 'urn:xmpp:hash-function-text-names:md5'
    HASHES_SHA1: str = 'urn:xmpp:hash-function-text-names:sha-1'
    HASHES_SHA256: str = 'urn:xmpp:hash-function-text-names:sha-256'
    HASHES_SHA512: str = 'urn:xmpp:hash-function-text-names:sha-512'
    HASHES_SHA3_256: str = 'urn:xmpp:hash-function-text-names:sha3-256'
    HASHES_SHA3_512: str = 'urn:xmpp:hash-function-text-names:sha3-512'
    HINTS: str = 'urn:xmpp:hints'
    HTTPUPLOAD_0: str = 'urn:xmpp:http:upload:0'
    HTTP_AUTH: str = 'http://jabber.org/protocol/http-auth'
    IBB: str = 'http://jabber.org/protocol/ibb'
    IDLE: str = 'urn:xmpp:idle:1'
    JINGLE: str = 'urn:xmpp:jingle:1'
    JINGLE_BYTESTREAM: str = 'urn:xmpp:jingle:transports:s5b:1'
    JINGLE_DTLS: str = 'urn:xmpp:jingle:apps:dtls:0'
    JINGLE_ERRORS: str = 'urn:xmpp:jingle:errors:1'
    JINGLE_FILE_TRANSFER: str = 'urn:xmpp:jingle:apps:file-transfer:3'
    JINGLE_FILE_TRANSFER_5: str = 'urn:xmpp:jingle:apps:file-transfer:5'
    JINGLE_IBB: str = 'urn:xmpp:jingle:transports:ibb:1'
    JINGLE_ICE_UDP: str = 'urn:xmpp:jingle:transports:ice-udp:1'
    JINGLE_RAW_UDP: str = 'urn:xmpp:jingle:transports:raw-udp:1'
    JINGLE_RTP: str = 'urn:xmpp:jingle:apps:rtp:1'
    JINGLE_RTP_AUDIO: str = 'urn:xmpp:jingle:apps:rtp:audio'
    JINGLE_RTP_VIDEO: str = 'urn:xmpp:jingle:apps:rtp:video'
    JINGLE_XTLS: str = 'urn:xmpp:jingle:security:xtls:0'
    LAST: str = 'jabber:iq:last'
    LOCATION: str = 'http://jabber.org/protocol/geoloc'
    MAM_1: str = 'urn:xmpp:mam:1'
    MAM_2: str = 'urn:xmpp:mam:2'
    MESSAGE_MODERATE: str = 'urn:xmpp:message-moderate:0'
    MESSAGE_RETRACT: str = 'urn:xmpp:message-retract:0'
    MOOD: str = 'http://jabber.org/protocol/mood'
    MSG_HINTS: str = 'urn:xmpp:hints'
    MUCLUMBUS: str = 'https://xmlns.zombofant.net/muclumbus/search/1.0'
    MUC: str = 'http://jabber.org/protocol/muc'
    MUC_USER: str = 'http://jabber.org/protocol/muc#user'
    MUC_ADMIN: str = 'http://jabber.org/protocol/muc#admin'
    MUC_OWNER: str = 'http://jabber.org/protocol/muc#owner'
    MUC_UNIQUE: str = 'http://jabber.org/protocol/muc#unique'
    MUC_CONFIG: str = 'http://jabber.org/protocol/muc#roomconfig'
    MUC_REQUEST: str = 'http://jabber.org/protocol/muc#request'
    MUC_INFO: str = 'http://jabber.org/protocol/muc#roominfo'
    NICK: str = 'http://jabber.org/protocol/nick'
    OMEMO_TEMP: str = 'eu.siacs.conversations.axolotl'
    OMEMO_TEMP_BUNDLE: str = 'eu.siacs.conversations.axolotl.bundles'
    OMEMO_TEMP_DL: str = 'eu.siacs.conversations.axolotl.devicelist'
    OPENPGP: str = 'urn:xmpp:openpgp:0'
    OPENPGP_PK: str = 'urn:xmpp:openpgp:0:public-keys'
    OPENPGP_SK: str = 'urn:xmpp:openpgp:0:secret-key'
    PING: str = 'urn:xmpp:ping'
    PRIVACY: str = 'jabber:iq:privacy'
    PRIVATE: str = 'jabber:iq:private'
    PUBKEY_ATTEST: str = 'urn:xmpp:attest:2'
    PUBKEY_PUBKEY: str = 'urn:xmpp:pubkey:2'
    PUBKEY_REVOKE: str = 'urn:xmpp:revoke:2'
    PUBSUB: str = 'http://jabber.org/protocol/pubsub'
    PUBSUB_ERROR: str = 'http://jabber.org/protocol/pubsub#errors'
    PUBSUB_CONFIG: str = 'http://jabber.org/protocol/pubsub#node_config'
    PUBSUB_EVENT: str = 'http://jabber.org/protocol/pubsub#event'
    PUBSUB_OWNER: str = 'http://jabber.org/protocol/pubsub#owner'
    PUBSUB_PUBLISH_OPTIONS: str = 'http://jabber.org/protocol/pubsub#publish-options'
    PUBSUB_NODE_MAX: str = 'http://jabber.org/protocol/pubsub#config-node-max'
    RECEIPTS: str = 'urn:xmpp:receipts'
    REGISTER: str = 'jabber:iq:register'
    REGISTER_FEATURE: str = 'http://jabber.org/features/iq-register'
    REPORTING: str = 'urn:xmpp:reporting:0'
    ROSTER: str = 'jabber:iq:roster'
    ROSTERNOTES: str = 'storage:rosternotes'
    ROSTERX: str = 'http://jabber.org/protocol/rosterx'
    ROSTER_VER: str = 'urn:xmpp:features:rosterver'
    RSM: str = 'http://jabber.org/protocol/rsm'
    SASL: str = 'urn:ietf:params:xml:ns:xmpp-sasl'
    SEARCH: str = 'jabber:iq:search'
    SECLABEL: str = 'urn:xmpp:sec-label:0'
    SECLABEL_CATALOG: str = 'urn:xmpp:sec-label:catalog:2'
    SESSION: str = 'urn:ietf:params:xml:ns:xmpp-session'
    SFS: str = 'urn:xmpp:sfs:0'
    SID: str = 'urn:xmpp:sid:0'
    SIGNED: str = 'jabber:x:signed'
    SIMS: str = 'urn:xmpp:sims:1'
    STANZAS: str = 'urn:ietf:params:xml:ns:xmpp-stanzas'
    STICKERS: str = 'urn:xmpp:stickers:0'
    STREAM: str = 'http://affinix.com/jabber/stream'
    STREAMS: str = 'http://etherx.jabber.org/streams'
    STREAM_MGMT: str = 'urn:xmpp:sm:3'
    STYLING: str = 'urn:xmpp:styling:0'
    TIME_REVISED: str = 'urn:xmpp:time'
    TIME: str = 'urn:xmpp:time'
    TLS: str = 'urn:ietf:params:xml:ns:xmpp-tls'
    TUNE: str = 'http://jabber.org/protocol/tune'
    URL_DATA: str = 'http://jabber.org/protocol/url-data'
    VCARD: str = 'vcard-temp'
    VCARD_UPDATE: str = 'vcard-temp:x:update'
    VCARD_CONVERSION: str = 'urn:xmpp:pep-vcard-conversion:0'
    VCARD4: str = 'urn:ietf:params:xml:ns:vcard-4.0'
    VCARD4_PUBSUB: str = 'urn:xmpp:vcard4'
    VERSION: str = 'jabber:iq:version'
    XHTML_IM: str = 'http://jabber.org/protocol/xhtml-im'
    XHTML: str = 'http://www.w3.org/1999/xhtml'
    XMPP_STREAMS: str = 'urn:ietf:params:xml:ns:xmpp-streams'
    X_OOB: str = 'jabber:x:oob' class NodeProcessed(Exception):
    """
    Exception that should be raised by handler when the handling should be
    stopped
    """ class InvalidFrom(StreamError):
    pass class InvalidJid(Exception):
    pass class InvalidStanza(Exception):
    pass class Iq(Protocol):
    """
    XMPP Iq object - get/set dialog mechanism
    """

    def __init__(self,
                 typ: Optional[str] = None,
                 queryNS: Optional[str] = None,
                 attrs: Optional[dict[str, str]] = None,
                 to: Optional[Union[JID, str]] = None,
                 frm: Optional[Union[JID, str]] = None,
                 payload: Optional[list[Union[Node, str]]] = None,
                 xmlns: str = Namespace.CLIENT,
                 node: Optional[Node] = None):
        """
        You can specify type, query namespace any additional attributes,
        recipient of the iq, sender of the iq, any additional payload (f.e.
        jabber:x:data node) and namespace in one go.

        Alternatively you can pass in the other XML object as the 'node'
        parameted to replicate it as an iq
        """
        Protocol.__init__(self,
                          'iq',
                          to=to,
                          typ=typ,
                          attrs=attrs,
                          frm=frm,
                          xmlns=xmlns,
                          node=node)
        if payload:
            self.setQueryPayload(payload)
        if queryNS:
            self.setQueryNS(queryNS)

    def getQuery(self) -> Optional[Node]:
        """
        Return the IQ's child element if it exists, None otherwise.
        """
        children = self.getChildren()
        if children and self.getType() != 'error' and \
        children[0].getName() != 'error':
            return children[0]
        return None

    def getQueryNS(self) -> Optional[str]:
        """
        Return the namespace of the 'query' child element
        """
        tag = self.getQuery()
        if tag:
            return tag.getNamespace()
        return None

    def getQuerynode(self) -> Optional[str]:
        """
        Return the 'node' attribute value of the 'query' child element
        """
        tag = self.getQuery()
        if tag:
            return tag.getAttr('node')
        return None

    def getQueryChildren(self) -> Optional[list[Node]]:
        """
        Return the 'query' child element child nodes
        """
        tag = self.getQuery()
        if tag:
            return tag.getChildren()
        return None

    def getQueryChild(self, name: Optional[str] = None) -> Optional[Node]:
        """
        Return the 'query' child element with name, or the first element
        which is not an error element
        """
        query = self.getQuery()
        if not query:
            return None
        for node in query.getChildren():
            if name is not None:
                if node.getName() == name:
                    return node
            else:
                if node.getName() != 'error':
                    return node
        return None

    def setQuery(self, name: Optional[str] = None) -> Node:
        """
        Change the name of the query node, creating it if needed. Keep the
        existing name if none is given (use 'query' if it's a creation).
        Return the query node.
        """
        query = self.getQuery()
        if query is None:
            query = self.addChild('query')
        if name is not None:
            query.setName(name)
        return query

    def setQueryNS(self, namespace: str) -> None:
        """
        Set the namespace of the 'query' child element
        """
        self.setQuery().setNamespace(namespace)

    def setQueryPayload(self, payload: list[Union[Node, str]]):
        """
        Set the 'query' child element payload
        """
        self.setQuery().setPayload(payload)

    def setQuerynode(self, node: str) -> None:
        """
        Set the 'node' attribute value of the 'query' child element
        """
        self.setQuery().setAttr('node', node)

    def buildReply(self, typ: str) -> Iq:
        """
        Build and return another Iq object of specified type. The to, from and
        query child node of new Iq are pre-set as reply to this Iq.
        """
        iq = Iq(typ,
                to=self.getFrom(),
                frm=self.getTo(),
                attrs={'id': self.getID()})
        iq.setQuery(self.getQuery().getName()).setNamespace(self.getQueryNS())
        return iq

    def buildSimpleReply(self, typ: str) -> Iq:
        return Iq(typ,
                  to=self.getFrom(),
                  attrs={'id': self.getID()}) class Presence(Protocol):

    def __init__(self,
                 to=None,
                 typ=None,
                 priority=None,
                 show=None,
                 status=None,
                 attrs=None,
                 frm=None,
                 timestamp=None,
                 payload=None,
                 xmlns=Namespace.CLIENT,
                 node=None):
        """
        You can specify recipient, type of message, priority, show and status
        values any additional attributes, sender of the presence, timestamp, any
        additional payload (f.e. jabber:x:delay element) and namespace in one
        go. Alternatively you can pass in the other XML object as the 'node'
        parameted to replicate it as presence
        """
        Protocol.__init__(self,
                          'presence',
                          to=to,
                          typ=typ,
                          attrs=attrs,
                          frm=frm,
                          payload=payload,
                          timestamp=timestamp,
                          xmlns=xmlns,
                          node=node)
        if priority:
            self.setPriority(priority)
        if show:
            self.setShow(show)
        if status:
            self.setStatus(status)

    def getPriority(self):
        """
        Return the priority of the message
        """
        return self.getTagData('priority')

    def getShow(self):
        """
        Return the show value of the message
        """
        return self.getTagData('show')

    def getStatus(self):
        """
        Return the status string of the message
        """
        return self.getTagData('status') or ''

    def setPriority(self, val):
        """
        Set the priority of the message
        """
        self.setTagData('priority', val)

    def setShow(self, val):
        """
        Set the show value of the message
        """
        if val not in ['away', 'chat', 'dnd', 'xa']:
            raise ValueError('Invalid show value: %s' % val)
        self.setTagData('show', val)

    def setStatus(self, val):
        """
        Set the status string of the message
        """
        self.setTagData('status', val)

    def _muc_getItemAttr(self, tag, attr):
        for xtag in self.getTags('x'):
            if xtag.getNamespace() not in (Namespace.MUC_USER,
                                           Namespace.MUC_ADMIN):
                continue
            for child in xtag.getTags(tag):
                return child.getAttr(attr)

    def _muc_getSubTagDataAttr(self, tag, attr):
        for xtag in self.getTags('x'):
            if xtag.getNamespace() not in (Namespace.MUC_USER,
                                           Namespace.MUC_ADMIN):
                continue
            for child in xtag.getTags('item'):
                for cchild in child.getTags(tag):
                    return cchild.getData(), cchild.getAttr(attr)
        return None, None

    def getRole(self):
        """
        Return the presence role (for groupchat)
        """
        return self._muc_getItemAttr('item', 'role')

    def getAffiliation(self):
        """
        Return the presence affiliation (for groupchat)
        """
        return self._muc_getItemAttr('item', 'affiliation')

    def getNewNick(self):
        """
        Return the status code of the presence (for groupchat)
        """
        return self._muc_getItemAttr('item', 'nick')

    def getJid(self):
        """
        Return the presence jid (for groupchat)
        """
        return self._muc_getItemAttr('item', 'jid')

    def getReason(self):
        """
        Returns the reason of the presence (for groupchat)
        """
        return self._muc_getSubTagDataAttr('reason', '')[0]

    def getActor(self):
        """
        Return the reason of the presence (for groupchat)
        """
        return self._muc_getSubTagDataAttr('actor', 'jid')[1]

    def getStatusCode(self):
        """
        Return the status code of the presence (for groupchat)
        """
        attrs = []
        for xtag in self.getTags('x'):
            for child in xtag.getTags('status'):
                attrs.append(child.getAttr('code'))
        return attrs class Message(Protocol):
    """
    XMPP Message stanza - "push" mechanism
    """

    def __init__(self,
                 to=None,
                 body=None,
                 xhtml=None,
                 typ=None,
                 subject=None,
                 attrs=None,
                 frm=None,
                 payload=None,
                 timestamp=None,
                 xmlns=Namespace.CLIENT,
                 node=None):
        """
        You can specify recipient, text of message, type of message any
        additional attributes, sender of the message, any additional payload
        (f.e. jabber:x:delay element) and namespace in one go.

        Alternatively you can pass in the other XML object as the 'node'
        parameted to replicate it as message
        """
        Protocol.__init__(self,
                          'message',
                          to=to,
                          typ=typ,
                          attrs=attrs,
                          frm=frm,
                          payload=payload,
                          timestamp=timestamp,
                          xmlns=xmlns,
                          node=node)
        if body:
            self.setBody(body)
        if xhtml is not None:
            self.setXHTML(xhtml)
        if subject is not None:
            self.setSubject(subject)

    def getBody(self):
        """
        Return text of the message
        """
        return self.getTagData('body')

    def getXHTML(self):
        return self.getTag('html', namespace=Namespace.XHTML_IM)

    def getSubject(self):
        """
        Return subject of the message
        """
        return self.getTagData('subject')

    def getThread(self):
        """
        Return thread of the message
        """
        return self.getTagData('thread')

    def getOriginID(self):
        """
        Return origin-id of the message
        """
        return self.getTagAttr('origin-id', namespace=Namespace.SID, attr='id')

    def getStanzaIDAttrs(self):
        """
        Return the stanza-id attributes of the message
        """
        try:
            attrs = self.getTag('stanza-id', namespace=Namespace.SID).getAttrs()
        except Exception:
            return None, None
        return attrs['id'], attrs['by']

    def setBody(self, val):
        """
        Set the text of the message"""
        self.setTagData('body', val)

    def setXHTML(self, body, add=False):
        if isinstance(body, str):
            body = Node(node=body)
        if add:
            xhtml = self.getTag('html', namespace=Namespace.XHTML_IM)
            if xhtml is not None:
                xhtml.addChild(node=body)
            else:
                self.addChild('html',
                              namespace=Namespace.XHTML_IM,
                              payload=body)
        else:
            xhtml_nodes = self.getTags('html', namespace=Namespace.XHTML_IM)
            for xhtml in xhtml_nodes:
                self.delChild(xhtml)
            self.addChild('html', namespace=Namespace.XHTML_IM, payload=body)

    def setSubject(self, val):
        """
        Set the subject of the message
        """
        self.setTagData('subject', val)

    def setThread(self, val):
        """
        Set the thread of the message
        """
        self.setTagData('thread', val)

    def setOriginID(self, val):
        """
        Sets the origin-id of the message
        """
        self.setTag('origin-id', namespace=Namespace.SID, attrs={'id': val})

    def buildReply(self, text=None):
        """
        Builds and returns another message object with specified text. The to,
        from, thread and type properties of new message are pre-set as reply to
        this message
        """
        m = Message(to=self.getFrom(),
                    frm=self.getTo(),
                    body=text,
                    typ=self.getType())
        th = self.getThread()
        if th:
            m.setThread(th)
        return m

    def getStatusCode(self):
        """
        Return the status code of the message (for groupchat config change)
        """
        attrs = []
        for xtag in self.getTags('x'):
            for child in xtag.getTags('status'):
                attrs.append(child.getAttr('code'))
        return attrs

    def setMarker(self, type_, id_):
        self.setTag(type_, namespace=Namespace.CHATMARKERS, attrs={'id': id_})

    def setMarkable(self):
        self.setTag('markable', namespace=Namespace.CHATMARKERS)

    def setReceiptRequest(self):
        self.setTag('request', namespace=Namespace.RECEIPTS)

    def setReceiptReceived(self, id_):
        self.setTag('received', namespace=Namespace.RECEIPTS, attrs={'id': id_})

    def setOOB(self, url, desc=None):
        oob = self.setTag('x', namespace=Namespace.X_OOB)
        oob.setTagData('url', url)
        if desc is not None:
            oob.setTagData('desc', desc)

    def setCorrection(self, id_):
        self.setTag('replace', namespace=Namespace.CORRECT, attrs={'id': id_})

    def setAttention(self):
        self.setTag('attention', namespace=Namespace.ATTENTION)

    def setHint(self, hint):
        self.setTag(hint, namespace=Namespace.HINTS) class Protocol(Node):
    """
    A "stanza" object class. Contains methods that are common for presences, iqs
    and messages
    """

    def __init__(self,
                 name=None,
                 to=None,
                 typ=None,
                 frm=None,
                 attrs=None,
                 payload=None,
                 timestamp=None,
                 xmlns=None,
                 node=None):
        """
        Constructor, name is the name of the stanza
        i.e. 'message' or 'presence'or 'iq'

        to is the value of 'to' attribure, 'typ' - 'type' attribute
        frn - from attribure, attrs - other attributes mapping,
        payload - same meaning as for simplexml payload definition
        timestamp - the time value that needs to be stamped over stanza
        xmlns - namespace of top stanza node
        node - parsed or unparsed stana to be taken as prototype.
        """
        if not attrs:
            attrs = {}
        if to:
            attrs['to'] = to
        if frm:
            attrs['from'] = frm
        if typ:
            attrs['type'] = typ
        Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node)
        if not node and xmlns:
            self.setNamespace(xmlns)
        if self['to']:
            self.setTo(self['to'])
        if self['from']:
            self.setFrom(self['from'])
        if (node and
                isinstance(node, Protocol) and
                self.__class__ == node.__class__
                and 'id' in self.attrs):
            del self.attrs['id']
        self.timestamp = None
        for d in self.getTags('delay', namespace=Namespace.DELAY2):
            try:
                if d.getAttr('stamp') < self.getTimestamp2():
                    self.setTimestamp(d.getAttr('stamp'))
            except Exception:
                pass
        if not self.timestamp:
            for x in self.getTags('x', namespace=Namespace.DELAY):
                try:
                    if x.getAttr('stamp') < self.getTimestamp():
                        self.setTimestamp(x.getAttr('stamp'))
                except Exception:
                    pass
        if timestamp is not None:
            self.setTimestamp(timestamp)

    def isError(self):
        return self.getAttr('type') == 'error'

    def isResult(self):
        return self.getAttr('type') == 'result'

    def getTo(self):
        """
        Return value of the 'to' attribute
        """
        try:
            return self['to']
        except Exception:
            pass
        return None

    def getFrom(self):
        """
        Return value of the 'from' attribute
        """
        try:
            return self['from']
        except Exception:
            pass
        return None

    def getTimestamp(self):
        """
        Return the timestamp in the 'yyyymmddThhmmss' format
        """
        if self.timestamp:
            return self.timestamp
        return time.strftime('%Y%m%dT%H:%M:%S', time.gmtime())

    def getTimestamp2(self):
        """
        Return the timestamp in the 'yyyymmddThhmmss' format
        """
        if self.timestamp:
            return self.timestamp
        return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())

    def getJid(self):
        """
        Return the value of the 'jid' attribute
        """
        attr = self.getAttr('jid')
        if attr:
            return JID.from_string(attr)
        return attr

    def getID(self) -> Optional[str]:
        """
        Return the value of the 'id' attribute
        """
        return self.getAttr('id')

    def setTo(self, val: Union[str, JID]):
        """
        Set the value of the 'to' attribute
        """
        if not isinstance(val, JID):
            val = JID.from_string(val)
        self.setAttr('to', val)

    def getType(self) -> Optional[str]:
        """
        Return the value of the 'type' attribute
        """
        return self.getAttr('type')

    def setFrom(self, val: Union[str, JID]):
        """
        Set the value of the 'from' attribute
        """
        if not isinstance(val, JID):
            val = JID.from_string(val)
        self.setAttr('from', val)

    def setType(self, val: str):
        """
        Set the value of the 'type' attribute
        """
        self.setAttr('type', val)

    def setID(self, val: str):
        """
        Set the value of the 'id' attribute
        """
        self.setAttr('id', val)

    def getError(self) -> Optional[str]:
        """
        Return the error-condition (if present) or the textual description
        of the error (otherwise)
        """
        errtag = self.getTag('error')
        if errtag is None:
            return None
        for tag in errtag.getChildren():
            if (tag.getName() != 'text' and
                    tag.getNamespace() == Namespace.STANZAS):
                return tag.getName()
        return None

    def getAppError(self):
        errtag = self.getTag('error')
        if errtag is None:
            return None
        for tag in errtag.getChildren():
            if (tag.getName() != 'text' and
                    tag.getNamespace() != Namespace.STANZAS):
                return tag.getName()
        return None

    def getAppErrorNamespace(self):
        errtag = self.getTag('error')
        if errtag is None:
            return None
        for tag in errtag.getChildren():
            if (tag.getName() != 'text' and
                    tag.getNamespace() != Namespace.STANZAS):
                return tag.getNamespace()
        return None

    def getErrorMsg(self):
        """
        Return the textual description of the error (if present)
        or the error condition
        """
        errtag = self.getTag('error')
        if errtag:
            for tag in errtag.getChildren():
                if tag.getName() == 'text':
                    return tag.getData()
            return self.getError()
        return None

    def getErrorCode(self):
        """
        Return the error code. Obsolete.
        """
        return self.getTagAttr('error', 'code')

    def getErrorType(self):
        """
        Return the error code. Obsolete.
        """
        return self.getTagAttr('error', 'type')

    def getStatusConditions(self, as_code=False):
        """
        Return the status conditions list as defined in XEP-0306.
        """
        result = set()
        status_tags = self.getTags('status')
        for status in status_tags:
            if as_code:
                code = status.getAttr('code')
                if code is not None:
                    result.add(code)
            else:
                for condition in status.getChildren():
                    result.add(condition.getName())
        return list(result)

    def setError(self, error, code=None):
        """
        Set the error code. Obsolete. Use error-conditions instead
        """
        if code:
            if str(code) in _errorcodes.keys():
                error = ErrorNode(_errorcodes[str(code)], text=error)
            else:
                error = ErrorNode(ERR_UNDEFINED_CONDITION, code=code,
                                  typ='cancel', text=error)
        elif isinstance(error, str):
            error = ErrorNode(error)
        self.setType('error')
        self.addChild(node=error)

    def setTimestamp(self, val=None):
        """
        Set the timestamp. timestamp should be the yyyymmddThhmmss string
        """
        if not val:
            val = time.strftime('%Y%m%dT%H:%M:%S', time.gmtime())
        self.timestamp=val
        self.setTag('x', {'stamp': self.timestamp}, namespace=Namespace.DELAY)

    def getProperties(self):
        """
        Return the list of namespaces to which belongs the
        direct childs of element
        """
        props = []
        for child in self.getChildren():
            prop = child.getNamespace()
            if prop not in props:
                props.append(prop)
        return props

    def getTag(self,
               name: str,
               attrs: Optional[dict[str, Any]] = None,
               namespace: Optional[str] = None,
               protocol: bool = False) -> Optional[Node]:
        """
        Return the Node instance for the tag.
        If protocol is True convert to a new Protocol/Message instance.
        """
        tag = Node.getTag(self, name, attrs, namespace)
        if protocol and tag:
            if name == 'message':
                return Message(node=tag)
            return Protocol(node=tag)
        return tag

    def __setitem__(self, item: str, val: Union[str, JID]):
        """
        Set the item 'item' to the value 'val'
        """
        if item in ['to', 'from']:
            if not isinstance(val, JID):
                val = JID.from_string(val)
        self.setAttr(item, val) class Error(Protocol):
    """
    Used to quickly transform received stanza into error reply
    """

    def __init__(self, node, error, reply=1):
        """
        Create error reply basing on the received 'node' stanza and the 'error'
        error condition

        If the 'node' is not the received stanza but locally created ('to' and
        'from' fields needs not swapping) specify the 'reply' argument as false.
        """
        if node.getType() == 'error':
            raise ValueError('Can’t create error reply from error')

        if reply:
            Protocol.__init__(self,
                              to=node.getFrom(),
                              node=node)
            self.delAttr('from')
        else:
            Protocol.__init__(self, node=node)
        self.setError(error) class StreamErrorNode(Node):
    def __init__(self, node):
        Node.__init__(self, node=node)

        self._text: dict[Optional[str], str] = {}

        text_elements = self.getTags('text', namespace=Namespace.XMPP_STREAMS)
        for element in text_elements:
            lang = element.getXmlLang()
            text = element.getData()
            self._text[lang] = text

    def get_condition(self) -> Optional[str]:
        for tag in self.getChildren():
            if (tag.getName() != 'text' and
                    tag.getNamespace() == Namespace.XMPP_STREAMS):
                return tag.getName()
        return None

    def get_text(self, pref_lang: Optional[str] = None) -> str:
        if pref_lang is not None:
            text = self._text.get(pref_lang)
            if text is not None:
                return text

        if self._text:
            text = self._text.get('en')
            if text is not None:
                return text

            text = self._text.get(None)
            if text is not None:
                return text
            return self._text.popitem()[1]
        return '' ERR_FEATURE_NOT_IMPLEMENTED = 'urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented' class EME(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_eme,
                          ns=Namespace.EME,
                          priority=40)
        ]

    def _process_eme(self, _client, stanza, properties):
        encryption = stanza.getTag('encryption', namespace=Namespace.EME)
        if encryption is None:
            return

        name = encryption.getAttr('name')
        namespace = encryption.getAttr('namespace')
        if namespace is None:
            self._log.warning('No namespace on message')
            return

        properties.eme = EMEData(name=name, namespace=namespace)
        self._log.info('Found data: %s', properties.eme) class HTTPAuth(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)
        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_http_auth,
                          ns=Namespace.HTTP_AUTH,
                          priority=40),
            StanzaHandler(name='iq',
                          callback=self._process_http_auth,
                          typ='get',
                          ns=Namespace.HTTP_AUTH,
                          priority=40)
        ]

    def _process_http_auth(self, _client, stanza, properties):
        confirm = stanza.getTag('confirm', namespace=Namespace.HTTP_AUTH)
        if confirm is None:
            return

        attrs = confirm.getAttrs()
        body = stanza.getTagData('body')
        id_ = attrs.get('id')
        method = attrs.get('method')
        url = attrs.get('url')
        properties.http_auth = HTTPAuthData(id_, method, url, body)
        self._log.info('HTTPAuth received: %s %s %s %s',
                       id_, method, url, body) class BasePresence(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='presence',
                          callback=self._process_presence_base,
                          priority=10),
        ]

    def _process_presence_base(self, _client, stanza, properties):
        properties.type = self._parse_type(stanza)
        properties.priority = self._parse_priority(stanza)
        properties.show = self._parse_show(stanza)
        properties.jid = stanza.getFrom()
        properties.id = stanza.getID()
        properties.status = stanza.getStatus()

        if properties.type.is_error:
            properties.error = error_factory(stanza)

        own_jid = self._client.get_bound_jid()
        properties.self_presence = own_jid == properties.jid
        properties.self_bare = properties.jid.bare_match(own_jid)

    def _parse_priority(self, stanza):
        priority = stanza.getPriority()
        if priority is None:
            return 0

        try:
            priority = int(priority)
        except Exception:
            self._log.warning('Invalid priority value: %s', priority)
            self._log.warning(stanza)
            return 0

        if priority not in range(-129, 128):
            self._log.warning('Invalid priority value: %s', priority)
            self._log.warning(stanza)
            return 0

        return priority

    def _parse_type(self, stanza):
        type_ = stanza.getType()
        try:
            return PresenceType(type_)
        except ValueError:
            self._log.warning('Presence with invalid type received')
            self._log.warning(stanza)
            self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
            raise NodeProcessed

    def _parse_show(self, stanza):
        show = stanza.getShow()
        if show is None:
            return PresenceShow.ONLINE
        try:
            return PresenceShow(stanza.getShow())
        except ValueError:
            self._log.warning('Presence with invalid show')
            self._log.warning(stanza)
            return PresenceShow.ONLINE

    @log_calls
    def unsubscribe(self, jid):
        self.send(jid=jid, typ='unsubscribe')

    @log_calls
    def unsubscribed(self, jid):
        self.send(jid=jid, typ='unsubscribed')

    @log_calls
    def subscribed(self, jid):
        self.send(jid=jid, typ='subscribed')

    @log_calls
    def subscribe(self, jid, status=None, nick=None):
        self.send(jid=jid, typ='subscribe', status=status, nick=nick)

    def send(self,
             jid=None,
             typ=None,
             priority=None,
             show=None,
             status=None,
             nick=None,
             caps=None,
             idle_time=None,
             signed=None,
             muc=False,
             muc_history=None,
             muc_password=None,
             extend=None):

        if show is not None and show not in ('chat', 'away', 'xa', 'dnd'):
            raise ValueError('Invalid show value: %s' % show)

        presence = Presence(jid, typ, priority, show, status)
        if nick is not None:
            nick_tag = presence.setTag('nick', namespace=Namespace.NICK)
            nick_tag.setData(nick)

        if idle_time is not None:
            idle_node = presence.setTag('idle', namespace=Namespace.IDLE)
            idle_node.setAttr('since', idle_time)

        if caps is not None and typ != 'unavailable':
            presence.setTag('c', namespace=Namespace.CAPS, attrs=caps)

        if signed is not None:
            presence.setTag(Namespace.SIGNED + ' x').setData(signed)

        if muc or muc_history is not None or muc_password is not None:
            muc_x = presence.setTag(Namespace.MUC + ' x')
            if muc_history is not None:
                muc_x.setTag('history', muc_history)

            if muc_password is not None:
                muc_x.setTagData('password', muc_password)

        if extend is not None:
            for node in extend:
                presence.addChild(node=node)

        self._client.send_stanza(presence) class BaseMessage(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_message_base,
                          priority=5),
            StanzaHandler(name='message',
                          callback=self._process_message_after_base,
                          priority=10),
        ]

    def _process_message_base(self, _client, stanza, properties):
        properties.type = self._parse_type(stanza)

        # Determine remote JID
        if properties.is_carbon_message and properties.carbon.is_sent:
            properties.jid = stanza.getTo()

        elif properties.is_mam_message and not properties.type.is_groupchat:
            own_jid = self._client.get_bound_jid()
            if own_jid.bare_match(stanza.getFrom()):
                properties.jid = stanza.getTo()
            else:
                properties.jid = stanza.getFrom()

        else:
            properties.jid = stanza.getFrom()

        properties.from_ = stanza.getFrom()
        properties.to = stanza.getTo()
        properties.id = stanza.getID()
        properties.self_message = self._parse_self_message(stanza, properties)

        # Stanza ID
        id_, by = stanza.getStanzaIDAttrs()
        if id_ is not None and by is not None:
            properties.stanza_id = StanzaIDData(id=id_, by=by)

        if properties.type.is_error:
            properties.error = error_factory(stanza)

    def _process_message_after_base(self, _client, stanza, properties):
        # This handler runs after decryption handlers had the chance
        # to decrypt the body
        properties.body = stanza.getBody()
        properties.thread = stanza.getThread()
        properties.subject = stanza.getSubject()
        forms = stanza.getTags('x', namespace=Namespace.DATA)
        if forms:
            properties.forms = forms

        xhtml = stanza.getXHTML()
        if xhtml is None:
            return

        if xhtml.getTag('body', namespace=Namespace.XHTML) is None:
            self._log.warning('xhtml without body found')
            self._log.warning(stanza)
            return

        properties.xhtml = XHTMLData(xhtml)

    def _parse_type(self, stanza):
        type_ = stanza.getType()
        if type_ is None:
            return MessageType.NORMAL

        try:
            return MessageType(type_)
        except ValueError:
            self._log.warning('Message with invalid type: %s', type_)
            self._log.warning(stanza)
            raise NodeProcessed

    @staticmethod
    def _parse_self_message(stanza, properties):
        if properties.type.is_groupchat:
            return False
        return stanza.getFrom().bare_match(stanza.getTo()) class BaseIq(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='iq',
                          callback=self._process_iq_base,
                          priority=10),
        ]

    def _process_iq_base(self, _client, stanza, properties):
        try:
            properties.type = IqType(stanza.getType())
        except ValueError:
            self._log.warning('Message with invalid type: %s', stanza.getType())
            self._log.warning(stanza)
            self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
            raise NodeProcessed

        properties.jid = stanza.getFrom()
        properties.id = stanza.getID()

        childs = stanza.getChildren()
        for child in childs:
            if child.getName() != 'error':
                properties.payload = child
                break

        properties.query = stanza.getQuery()

        if properties.type.is_error:
            properties.error = error_factory(stanza) class Nickname(BaseModule):

    _depends = {
        'publish': 'PubSub'
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_pubsub_nickname,
                          ns=Namespace.PUBSUB_EVENT,
                          priority=16),
            StanzaHandler(name='message',
                          callback=self._process_nickname,
                          ns=Namespace.NICK,
                          priority=40),
            StanzaHandler(name='presence',
                          callback=self._process_nickname,
                          ns=Namespace.NICK,
                          priority=40),
        ]

    def _process_nickname(self, _client, stanza, properties):
        if stanza.getName() == 'message':
            properties.nickname = self._parse_nickname(stanza)

        elif stanza.getName() == 'presence':
            # the nickname MUST NOT be included in presence broadcasts
            # (i.e., <presence/> stanzas with no 'type' attribute or
            # of type "unavailable").
            if properties.type in (PresenceType.AVAILABLE,
                                   PresenceType.UNAVAILABLE):
                return
            properties.nickname = self._parse_nickname(stanza)

    def _process_pubsub_nickname(self, _client, _stanza, properties):
        if not properties.is_pubsub_event:
            return

        if properties.pubsub_event.node != Namespace.NICK:
            return

        item = properties.pubsub_event.item
        if item is None:
            # Retract, Deleted or Purged
            return

        nick = self._parse_nickname(item)
        if nick is None:
            self._log.info('Received nickname: %s - nickname removed',
                           properties.jid)
            return

        self._log.info('Received nickname: %s - %s', properties.jid, nick)
        properties.pubsub_event = properties.pubsub_event._replace(data=nick)

    @staticmethod
    def _parse_nickname(stanza):
        nickname = stanza.getTag('nick', namespace=Namespace.NICK)
        if nickname is None:
            return None
        return nickname.getData() or None

    @iq_request_task
    def set_nickname(self, nickname, public=False):
        task = yield

        access_model = 'open' if public else 'presence'

        options = {
            'pubsub#persist_items': 'true',
            'pubsub#access_model': access_model,
        }

        item = Node('nick', {'xmlns': Namespace.NICK})
        if nickname is not None:
            item.addData(nickname)

        result = yield self.publish(Namespace.NICK,
                                    item,
                                    id_='current',
                                    options=options,
                                    force_node_options=True)

        yield finalize(task, result)

    @iq_request_task
    def set_access_model(self, public):
        task = yield

        access_model = 'open' if public else 'presence'

        result = yield self._client.get_module('PubSub').set_access_model(
            Namespace.NICK, access_model)

        yield finalize(task, result) class Delay(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_message_delay,
                          ns=Namespace.DELAY2,
                          priority=15),
            StanzaHandler(name='presence',
                          callback=self._process_presence_delay,
                          ns=Namespace.DELAY2,
                          priority=15)
        ]

    def _process_message_delay(self, _client, stanza, properties):
        if properties.is_muc_subject:
            # MUC Subjects can have a delay timestamp
            # to indicate when the user has set the subject,
            # the 'from' attr on these delays is the MUC server
            # but we treat it as user timestamp
            jids = [properties.jid.bare,
                    properties.jid.domain]

            properties.user_timestamp = parse_delay(stanza, from_=jids)

        else:
            if properties.from_muc:
                # Some servers use the MUC JID, others the domain
                jids = [properties.jid.bare,
                        properties.jid.domain]
            else:
                jids = [self._client.get_bound_jid().domain]

            server_delay = parse_delay(stanza, from_=jids)
            if server_delay is not None:
                properties.has_server_delay = True
                properties.timestamp = server_delay

            properties.user_timestamp = parse_delay(stanza, not_from=jids)

    @staticmethod
    def _process_presence_delay(_client, stanza, properties):
        properties.user_timestamp = parse_delay(stanza) class MUC(BaseModule):

    _depends = {
        'disco_info': 'Discovery',
        'request_vcard': 'VCardTemp',
        'send_retract_request': 'Moderation',
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='presence',
                          callback=self._process_muc_presence,
                          ns=Namespace.MUC,
                          priority=11),
            StanzaHandler(name='presence',
                          callback=self._process_muc_user_presence,
                          ns=Namespace.MUC_USER,
                          priority=11),
            StanzaHandler(name='message',
                          callback=self._process_message_before_decryption,
                          typ='groupchat',
                          priority=6),
            StanzaHandler(name='message',
                          callback=self._process_message_after_decryption,
                          typ='groupchat',
                          priority=16),
            StanzaHandler(name='message',
                          callback=self._process_mediated_invite,
                          typ='normal',
                          ns=Namespace.MUC_USER,
                          priority=11),
            StanzaHandler(name='message',
                          callback=self._process_direct_invite,
                          typ='normal',
                          ns=Namespace.CONFERENCE,
                          priority=12),
            StanzaHandler(name='message',
                          callback=self._process_voice_request,
                          ns=Namespace.DATA,
                          priority=11),
            StanzaHandler(name='message',
                          callback=self._process_message,
                          ns=Namespace.MUC_USER,
                          priority=13),
        ]

    @staticmethod
    def _process_muc_presence(_client, stanza, properties):
        muc = stanza.getTag('x', namespace=Namespace.MUC)
        if muc is None:
            return
        properties.from_muc = True
        properties.muc_jid = properties.jid.new_as_bare()
        properties.muc_nickname = properties.jid.resource

    def _process_muc_user_presence(self, _client, stanza, properties):
        muc_user = stanza.getTag('x', namespace=Namespace.MUC_USER)
        if muc_user is None:
            return
        properties.from_muc = True
        properties.muc_jid = properties.jid.new_as_bare()

        destroy = muc_user.getTag('destroy')
        if destroy is not None:
            alternate = destroy.getAttr('jid')
            if alternate is not None:
                try:
                    alternate = JID.from_string(alternate)
                except Exception as error:
                    self._log.warning('Invalid alternate JID provided: %s',
                                      error)
                    self._log.warning(stanza)
                    alternate = None
            properties.muc_destroyed = MucDestroyed(
                alternate=alternate,
                reason=muc_user.getTagData('reason'),
                password=muc_user.getTagData('password'))
            return

        properties.muc_nickname = properties.jid.resource

        # https://xmpp.org/extensions/xep-0045.html#registrar-statuscodes
        message_status_codes = [
            StatusCode.NON_ANONYMOUS,
            StatusCode.SELF,
            StatusCode.CONFIG_ROOM_LOGGING,
            StatusCode.CREATED,
            StatusCode.NICKNAME_MODIFIED,
            StatusCode.REMOVED_BANNED,
            StatusCode.NICKNAME_CHANGE,
            StatusCode.REMOVED_KICKED,
            StatusCode.REMOVED_AFFILIATION_CHANGE,
            StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY,
            StatusCode.REMOVED_SERVICE_SHUTDOWN,
            StatusCode.REMOVED_ERROR,
        ]

        codes = set()
        for status in muc_user.getTags('status'):
            try:
                code = StatusCode(status.getAttr('code'))
            except ValueError:
                self._log.warning('Received invalid status code: %s',
                                  status.getAttr('code'))
                self._log.warning(stanza)
                continue
            if code in message_status_codes:
                codes.add(code)

        if codes:
            properties.muc_status_codes = codes

        try:
            properties.muc_user = parse_muc_user(muc_user)
        except StanzaMalformed as error:
            self._log.warning(error)
            self._log.warning(stanza)
            raise NodeProcessed

        if (properties.muc_user is not None and
                properties.muc_user.role.is_none and
                not properties.type.is_unavailable):
            self._log.warning('Malformed Stanza')
            self._log.warning(stanza)
            raise NodeProcessed

    def _process_message_before_decryption(self, _client, stanza, properties):
        properties.from_muc = True
        properties.muc_jid = properties.jid.new_as_bare()
        properties.muc_nickname = properties.jid.resource

        muc_user = stanza.getTag('x', namespace=Namespace.MUC_USER)
        if muc_user is not None:
            try:
                properties.muc_user = parse_muc_user(muc_user,
                                                     is_presence=False)
            except StanzaMalformed as error:
                self._log.warning(error)
                self._log.warning(stanza)
                raise NodeProcessed

        addresses = stanza.getTag('addresses', namespace=Namespace.ADDRESS)
        if addresses is not None:
            address = addresses.getTag('address', attrs={'type': 'ofrom'})
            if address is not None:
                properties.muc_ofrom = JID.from_string(address.getAttr('jid'))

    def _process_message_after_decryption(self, _client, stanza, properties):
        if properties.body is None and properties.subject:
            properties.muc_subject = MucSubject(
                text=properties.subject,
                author=properties.muc_nickname,
                timestamp=properties.user_timestamp)

    def _process_message(self, _client, stanza, properties):
        muc_user = stanza.getTag('x', namespace=Namespace.MUC_USER)
        if muc_user is None:
            return

        # MUC Private message
        if (properties.type.is_chat or
                properties.type.is_error and
                not muc_user.getChildren()):
            properties.muc_private_message = True
            return

        if properties.is_muc_invite_or_decline:
            return

        properties.from_muc = True
        properties.muc_jid = properties.jid.new_as_bare()

        if not properties.jid.is_bare:
            return

        # MUC Config change
        # https://xmpp.org/extensions/xep-0045.html#registrar-statuscodes
        message_status_codes = [
            StatusCode.SHOWING_UNAVAILABLE,
            StatusCode.NOT_SHOWING_UNAVAILABLE,
            StatusCode.CONFIG_NON_PRIVACY_RELATED,
            StatusCode.CONFIG_ROOM_LOGGING,
            StatusCode.CONFIG_NO_ROOM_LOGGING,
            StatusCode.CONFIG_NON_ANONYMOUS,
            StatusCode.CONFIG_SEMI_ANONYMOUS,
            StatusCode.CONFIG_FULL_ANONYMOUS
        ]

        codes = set()
        for status in muc_user.getTags('status'):
            try:
                code = StatusCode(status.getAttr('code'))
            except ValueError:
                self._log.warning('Received invalid status code: %s',
                                  status.getAttr('code'))
                self._log.warning(stanza)
                continue
            if code in message_status_codes:
                codes.add(code)

        if codes:
            properties.muc_status_codes = codes

    @staticmethod
    def _process_direct_invite(_client, stanza, properties):
        direct = stanza.getTag('x', namespace=Namespace.CONFERENCE)
        if direct is None:
            return

        if stanza.getTag('x', namespace=Namespace.MUC_USER) is not None:
            # not a direct invite
            # See https://xmpp.org/extensions/xep-0045.html#example-57
            # read implementation notes
            return

        data = {}
        data['muc'] = JID.from_string(direct.getAttr('jid'))
        data['from_'] = properties.jid
        data['reason'] = direct.getAttr('reason')
        data['password'] = direct.getAttr('password')
        data['continued'] = direct.getAttr('continue') == 'true'
        data['thread'] = direct.getAttr('thread')
        data['type'] = InviteType.DIRECT
        properties.muc_invite = InviteData(**data)

    @staticmethod
    def _process_mediated_invite(_client, stanza, properties):
        muc_user = stanza.getTag('x', namespace=Namespace.MUC_USER)
        if muc_user is None:
            return

        if properties.type != MessageType.NORMAL:
            return

        properties.from_muc = True
        properties.muc_jid = properties.jid.new_as_bare()

        data = {}

        invite = muc_user.getTag('invite')
        if invite is not None:
            data['muc'] = properties.jid.new_as_bare()
            data['from_'] = JID.from_string(invite.getAttr('from'))
            data['reason'] = invite.getTagData('reason')
            data['password'] = muc_user.getTagData('password')
            data['type'] = InviteType.MEDIATED

            data['continued'] = False
            data['thread'] = None
            continue_ = invite.getTag('continue')
            if continue_ is not None:
                data['continued'] = True
                data['thread'] = continue_.getAttr('thread')
            properties.muc_invite = InviteData(**data)
            return

        decline = muc_user.getTag('decline')
        if decline is not None:
            data['muc'] = properties.jid.new_as_bare()
            data['from_'] = JID.from_string(decline.getAttr('from'))
            data['reason'] = decline.getTagData('reason')
            properties.muc_decline = DeclineData(**data)
            return

    def _process_voice_request(self, _client, stanza, properties):
        data_form = stanza.getTag('x', namespace=Namespace.DATA)
        if data_form is None:
            return

        data_form = extend_form(data_form)
        try:
            if data_form['FORM_TYPE'].value != Namespace.MUC_REQUEST:
                return
        except KeyError:
            return

        nick = data_form['muc#roomnick'].value

        try:
            jid = JID.from_string(data_form['muc#jid'].value)
        except Exception:
            self._log.warning('Invalid JID on voice request')
            self._log.warning(stanza)
            raise NodeProcessed

        properties.voice_request = VoiceRequest(jid=jid,
                                                nick=nick,
                                                form=data_form)
        properties.from_muc = True
        properties.muc_jid = properties.jid.new_as_bare()

    def approve_voice_request(self, muc_jid, voice_request):
        form = voice_request.form
        form.type_ = 'submit'
        form['muc#request_allow'].value = True
        self._client.send_stanza(Message(to=muc_jid, payload=form))

    @iq_request_task
    def get_affiliation(self, jid, affiliation):
        _task = yield

        response = yield make_affiliation_request(jid, affiliation)
        if response.isError():
            raise StanzaError(response)

        room_jid = response.getFrom()
        query = response.getTag('query', namespace=Namespace.MUC_ADMIN)
        items = query.getTags('item')
        users_dict = {}
        for item in items:
            try:
                jid = JID.from_string(item.getAttr('jid'))
            except Exception as error:
                self._log.warning('Invalid JID: %s, %s',
                                  item.getAttr('jid'), error)
                continue

            users_dict[jid] = {}
            if item.has_attr('nick'):
                users_dict[jid]['nick'] = item.getAttr('nick')
            if item.has_attr('role'):
                users_dict[jid]['role'] = item.getAttr('role')
            reason = item.getTagData('reason')
            if reason:
                users_dict[jid]['reason'] = reason

        self._log.info('Affiliations received from %s: %s',
                       room_jid, users_dict)

        yield AffiliationResult(jid=room_jid, users=users_dict)

    @iq_request_task
    def destroy(self, room_jid, reason=None, jid=None):
        _task = yield

        response = yield make_destroy_request(room_jid, reason, jid)
        yield process_response(response)

    @iq_request_task
    def request_info(self, jid, request_vcard=True, allow_redirect=False):
        _task = yield

        redirected = False

        disco_info = yield self.disco_info(jid)
        if is_error(disco_info):
            error_response = disco_info

            if not allow_redirect:
                raise error_response

            if error_response.condition != 'gone':
                raise error_response

            try:
                jid = parse_xmpp_uri(error_response.condition_data)[0]
            except Exception:
                raise error_response

            redirected = True
            disco_info = yield self.disco_info(jid)
            raise_if_error(disco_info)

        if not request_vcard or not disco_info.supports(Namespace.VCARD):
            yield MucInfoResult(info=disco_info, redirected=redirected)

        vcard = yield self.request_vcard(jid)
        if is_error(vcard):
            yield MucInfoResult(info=disco_info, redirected=redirected)

        yield MucInfoResult(info=disco_info,
                            vcard=vcard,
                            redirected=redirected)

    @iq_request_task
    def set_config(self, room_jid, form):
        _task = yield

        response = yield make_set_config_request(room_jid, form)
        yield process_response(response)

    @iq_request_task
    def request_config(self, room_jid):
        task = yield

        response = yield make_config_request(room_jid)
        if response.isError():
            raise StanzaError(response)

        jid = response.getFrom()
        children = response.getQueryChildren()

        for form in children:
            if form.getNamespace() == Namespace.DATA:
                dataform = extend_form(node=form)
                self._log.info('Config form received for %s', jid)
                yield MucConfigResult(jid=jid, form=dataform)

        yield MucConfigResult(jid=jid)

    @iq_request_task
    def cancel_config(self, room_jid):
        _task = yield

        response = yield make_cancel_config_request(room_jid)
        yield process_response(response)

    @iq_request_task
    def set_affiliation(self, room_jid, users_dict):
        _task = yield

        response = yield make_set_affiliation_request(room_jid, users_dict)
        yield process_response(response)

    @iq_request_task
    def set_role(self, room_jid, nick, role, reason=None):
        _task = yield

        response = yield make_set_role_request(room_jid, nick, role, reason)
        yield process_response(response)

    def retract_message(self, room_jid: JID, stanza_id: str,
                        reason: Optional[str] = None) -> None:
        self.send_retract_request(room_jid, stanza_id, reason)

    def set_subject(self, room_jid, subject):
        message = Message(room_jid, typ='groupchat', subject=subject)
        self._log.info('Set subject for %s', room_jid)
        self._client.send_stanza(message)

    def decline(self, room, to, reason=None):
        message = Message(to=room)
        muc_user = message.addChild('x', namespace=Namespace.MUC_USER)
        decline = muc_user.addChild('decline', attrs={'to': to})
        if reason:
            decline.setTagData('reason', reason)
        self._client.send_stanza(message)

    def request_voice(self, room):
        message = Message(to=room)
        xdata = DataForm(typ='submit')
        xdata.addChild(node=DataField(name='FORM_TYPE',
                                      value=Namespace.MUC_REQUEST))
        xdata.addChild(node=DataField(name='muc#role',
                                      value='participant',
                                      typ='text-single'))
        message.addChild(node=xdata)
        self._client.send_stanza(message)

    def invite(self, room, to, password, reason=None, continue_=False,
               type_=InviteType.MEDIATED):
        if type_ == InviteType.DIRECT:
            invite = build_direct_invite(
                room, to, reason, password, continue_)
        else:
            invite = build_mediated_invite(
                room, to, reason, password, continue_)
        return self._client.send_stanza(invite)

    @iq_request_task
    def send_captcha(self, room_jid, form_node):
        _task = yield

        response = yield make_captcha_request(room_jid, form_node)
        yield process_response(response)

    def cancel_captcha(self, room_jid, message_id):
        message = Message(typ='error', to=room_jid)
        message.setID(message_id)
        message.setError(ERR_NOT_ACCEPTABLE)
        self._client.send_stanza(message) class Moderation(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_message,
                          typ='groupchat',
                          ns=Namespace.FASTEN,
                          priority=20),
        ]

    @iq_request_task
    def send_retract_request(self, muc_jid: JID, stanza_id: str,
                             reason: Optional[str] = None):
        _task = yield

        response = yield _make_retract_request(muc_jid, stanza_id, reason)

        yield process_response(response)

    @staticmethod
    def _process_message(_client, stanza: Node,
                        properties: MessageProperties) -> None:
        if not properties.jid.is_bare:
            return

        apply_to = stanza.getTag(
            'apply-to', namespace=Namespace.FASTEN)
        if apply_to is None:
            return

        moderated = apply_to.getTag(
            'moderated', namespace=Namespace.MESSAGE_MODERATE)
        if moderated is None:
            return

        retract = moderated.getTag(
                'retract', namespace=Namespace.MESSAGE_RETRACT)
        if retract is None:
            # Tag can be 'retract' or 'retracted', depending on whether the
            # server applies a tombstone for MAM messages or not.
            retract = moderated.getTag(
                'retracted', namespace=Namespace.MESSAGE_RETRACT)
        if retract is None:
            return

        properties.moderation = ModerationData(
            stanza_id=apply_to.getAttr('id'),
            moderator_jid=moderated.getAttr('by'),
            reason=moderated.getTagData('reason'),
            timestamp=retract.getAttr('stamp')) class Idle(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='presence',
                          callback=self._process_idle,
                          ns=Namespace.IDLE,
                          priority=15)
        ]

    def _process_idle(self, _client, stanza, properties):
        idle_tag = stanza.getTag('idle', namespace=Namespace.IDLE)
        if idle_tag is None:
            return

        since = idle_tag.getAttr('since')
        if since is None:
            self._log.warning('No since attr in idle node')
            self._log.warning(stanza)
            return

        timestamp = parse_datetime(since, convert='utc', epoch=True)
        if timestamp is None:
            self._log.warning('Invalid timestamp received: %s', since)
            self._log.warning(stanza)

        properties.idle_timestamp = timestamp class PGPLegacy(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_pgplegacy_message,
                          ns=Namespace.ENCRYPTED,
                          priority=7),
            StanzaHandler(name='presence',
                          callback=self._process_signed,
                          ns=Namespace.SIGNED,
                          priority=15)
        ]

    @staticmethod
    def _process_signed(_client, stanza, properties):
        signed = stanza.getTag('x', namespace=Namespace.SIGNED)
        if signed is None:
            return

        properties.signed = signed.getData()

    def _process_pgplegacy_message(self, _client, stanza, properties):
        pgplegacy = stanza.getTag('x', namespace=Namespace.ENCRYPTED)
        if pgplegacy is None:
            self._log.warning('No x node found')
            self._log.warning(stanza)
            return

        data = pgplegacy.getData()
        if not data:
            self._log.warning('No data in x node found')
            self._log.warning(stanza)
            return

        self._log.info('Encrypted message received')
        properties.pgp_legacy = data class VCardAvatar(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='presence',
                          callback=self._process_avatar,
                          ns=Namespace.VCARD_UPDATE,
                          priority=15)
        ]

    def _process_avatar(self, _client, stanza, properties):
        if properties.type != PresenceType.AVAILABLE:
            return

        update = stanza.getTag('x', namespace=Namespace.VCARD_UPDATE)
        if update is None:
            return

        avatar_sha = update.getTagData('photo')
        if avatar_sha is None:
            properties.avatar_state = AvatarState.NOT_READY
            self._log.info('%s is not ready to promote an avatar',
                           stanza.getFrom())
            # Empty update element, ignore
            return

        if avatar_sha == '':
            properties.avatar_state = AvatarState.EMPTY
            self._log.info('%s empty avatar advertised', stanza.getFrom())
            return

        properties.avatar_sha = avatar_sha
        properties.avatar_state = AvatarState.ADVERTISED
        self._log.info('%s advertises %s', stanza.getFrom(), avatar_sha) class Captcha(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_captcha,
                          ns=Namespace.CAPTCHA,
                          priority=40),
        ]

    def _process_captcha(self, _client, stanza, properties):
        captcha = stanza.getTag('captcha', namespace=Namespace.CAPTCHA)
        if captcha is None:
            return

        data_form = captcha.getTag('x', namespace=Namespace.DATA)
        if data_form is None:
            self._log.warning('Invalid captcha form')
            self._log.warning(stanza)
            return

        form = extend_form(node=data_form)
        bob_data = parse_bob_data(stanza)

        properties.captcha = CaptchaData(form=form,
                                         bob_data=bob_data) class EntityCaps(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='presence',
                          callback=self._process_entity_caps,
                          ns=Namespace.CAPS,
                          priority=15),
            StanzaHandler(name='iq',
                          callback=self._process_disco_info,
                          typ='get',
                          ns=Namespace.DISCO_INFO,
                          priority=20),
        ]

        self._identities = []
        self._features = []

        self._uri = None
        self._node = None
        self._caps = None
        self._caps_hash = None

    def _process_disco_info(self, client, stanza, _properties):
        if self._caps is None:
            return

        node = stanza.getQuerynode()
        if node is not None:
            if self._node != node:
                return

        iq = stanza.buildReply('result')
        if node is not None:
            iq.setQuerynode(node)

        query = iq.getQuery()
        for identity in self._caps.identities:
            query.addChild(node=identity.get_node())

        for feature in self._caps.features:
            query.addChild('feature', attrs={'var': feature})

        self._log.info('Respond with disco info')
        client.send_stanza(iq)
        raise NodeProcessed

    def _process_entity_caps(self, _client, stanza, properties):
        caps = stanza.getTag('c', namespace=Namespace.CAPS)
        if caps is None:
            return

        hash_algo = caps.getAttr('hash')
        if hash_algo != 'sha-1':
            self._log.warning('Unsupported hashing algorithm used: %s',
                              hash_algo)
            self._log.warning(stanza)
            return

        node = caps.getAttr('node')
        if not node:
            self._log.warning('node attribute missing')
            self._log.warning(stanza)
            return

        ver = caps.getAttr('ver')
        if not ver:
            self._log.warning('ver attribute missing')
            self._log.warning(stanza)
            return

        properties.entity_caps = EntityCapsData(
            hash=hash_algo,
            node=node,
            ver=ver)

    @property
    def caps(self):
        if self._caps is None:
            return None
        return EntityCapsData(hash='sha-1',
                              node=self._uri,
                              ver=self._caps_hash)

    def set_caps(self, identities, features, uri):
        self._uri = uri
        self._caps = DiscoInfo(None, identities, features, [])
        self._caps_hash = compute_caps_hash(self._caps, compare=False)
        self._node = '%s#%s' % (uri, self._caps_hash) class Blocking(BaseModule):
    def __init__(self, client: Client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='iq',
                          priority=15,
                          callback=self._process_blocking_push,
                          typ='set',
                          ns=Namespace.BLOCKING),
        ]

    @iq_request_task
    def request_blocking_list(self):
        _task = yield

        result = yield _make_blocking_list_request()
        if result.isError():
            raise StanzaError(result)

        blocklist = result.getTag('blocklist', namespace=Namespace.BLOCKING)
        if blocklist is None:
            raise MalformedStanzaError('blocklist node missing', result)

        blocked = set()
        for item in blocklist.getTags('item'):
            try:
                jid = JID.from_string(item.getAttr('jid'))
            except Exception:
                self._log.info('Invalid JID: %s', item.getAttr('jid'))
                continue

            blocked.add(jid)

        self._log.info('Received blocking list: %s', blocked)
        yield blocked

    @iq_request_task
    def block(self,
              jids: list[JID],
              report: Optional[BlockingReportValues] = None):

        _task = yield

        response = yield _make_block_request(jids, report)
        yield process_response(response)

    @iq_request_task
    def unblock(self, jids: list[JID]):
        _task = yield

        response = yield _make_unblock_request(jids)
        yield process_response(response)

    @staticmethod
    def _process_blocking_push(client: Client,
                               stanza: Iq,
                               properties: BlockingProperties):

        unblock = stanza.getTag('unblock', namespace=Namespace.BLOCKING)
        if unblock is not None:
            properties.blocking = _parse_push(unblock)

        block = stanza.getTag('block', namespace=Namespace.BLOCKING)
        if block is not None:
            properties.blocking = _parse_push(block)

        reply = stanza.buildSimpleReply('result')
        client.send_stanza(reply) class PubSub(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_pubsub_base,
                          ns=Namespace.PUBSUB_EVENT,
                          priority=15),
        ]

    def _process_pubsub_base(self, _client, stanza, properties):
        properties.pubsub = True
        event = stanza.getTag('event', namespace=Namespace.PUBSUB_EVENT)

        delete = event.getTag('delete')
        if delete is not None:
            node = delete.getAttr('node')
            properties.pubsub_event = PubSubEventData(
                node, deleted=True)
            return

        purge = event.getTag('purge')
        if purge is not None:
            node = purge.getAttr('node')
            properties.pubsub_event = PubSubEventData(node, purged=True)
            return

        items = event.getTag('items')
        if items is not None:
            node = items.getAttr('node')

            retract = items.getTag('retract')
            if retract is not None:
                id_ = retract.getAttr('id')
                properties.pubsub_event = PubSubEventData(
                    node, id_, retracted=True)
                return

            if len(items.getChildren()) != 1:
                self._log.warning('PubSub event with != 1 item')
                self._log.warning(stanza)
                return

            item = items.getTag('item')
            if item is None:
                self._log.warning('No item node found')
                self._log.warning(stanza)
                return
            id_ = item.getAttr('id')
            properties.pubsub_event = PubSubEventData(node, id_, item)

    @iq_request_task
    def request_item(self, node, id_, jid=None):
        task = yield

        response = yield _make_pubsub_request(node, id_=id_, jid=jid)

        if response.isError():
            raise PubSubStanzaError(response)

        item = _get_pubsub_item(response, node, id_)
        yield task.set_result(item)

    @iq_request_task
    def request_items(self, node, max_items=None, jid=None):
        _task = yield

        response = yield _make_pubsub_request(node,
                                              max_items=max_items,
                                              jid=jid)

        if response.isError():
            raise PubSubStanzaError(response)

        yield _get_pubsub_items(response, node)

    @iq_request_task
    def publish(self,
                node,
                item,
                id_=None,
                options=None,
                jid=None,
                force_node_options=False):

        _task = yield

        request = _make_publish_request(node, item, id_, options, jid)
        response = yield request

        if response.isError():
            error = PubSubStanzaError(response)
            if (not force_node_options or
                    error.app_condition != 'precondition-not-met'):
                raise error

            result = yield self.reconfigure_node(node, options, jid)
            if is_error(result):
                raise result

            response = yield request
            if response.isError():
                raise PubSubStanzaError(response)

        jid = response.getFrom()
        item_id = _get_published_item_id(response, node, id_)
        yield PubSubPublishResult(jid, node, item_id)

    @iq_request_task
    def get_access_model(self, node):
        _task = yield

        result = yield self.get_node_configuration(node)

        raise_if_error(result)

        yield result.form['pubsub#access_model'].value

    @iq_request_task
    def set_access_model(self, node, model):
        task = yield

        if model not in ('open', 'presence'):
            raise ValueError('Invalid access model')

        result = yield self.get_node_configuration(node)

        raise_if_error(result)

        try:
            access_model = result.form['pubsub#access_model'].value
        except Exception:
            yield task.set_error('warning',
                                 condition='access-model-not-supported')

        if access_model == model:
            jid = self._client.get_bound_jid().new_as_bare()
            yield CommonResult(jid=jid)

        result.form['pubsub#access_model'].value = model

        result = yield self.set_node_configuration(node, result.form)

        yield finalize(task, result)

    @iq_request_task
    def retract(self, node, id_, jid=None, notify=True):
        _task = yield

        response = yield _make_retract_request(node, id_, jid, notify)
        yield process_response(response)

    @iq_request_task
    def purge(self, node, jid=None):
        _task = yield

        response = yield _make_purge_request(node, jid)
        yield process_response(response)

    @iq_request_task
    def delete(self, node, jid=None):
        _task = yield

        response = yield _make_delete_request(node, jid)
        yield process_response(response)

    @iq_request_task
    def reconfigure_node(self, node, options, jid=None):
        _task = yield

        result = yield self.get_node_configuration(node, jid)
        if is_error(result):
            raise result

        _apply_options(result.form, options)
        result = yield self.set_node_configuration(node, result.form, jid)
        yield result

    @iq_request_task
    def set_node_configuration(self, node, form, jid=None):
        _task = yield

        response = yield _make_node_configuration(node, form, jid)
        yield process_response(response)

    @iq_request_task
    def get_node_configuration(self, node, jid=None):
        _task = yield

        response = yield _make_node_configuration_request(node, jid)

        if response.isError():
            raise PubSubStanzaError(response)

        jid = response.getFrom()
        form = _get_configure_form(response, node)
        yield PubSubNodeConfigurationResult(jid=jid, node=node, form=form) class Activity(BaseModule):

    _depends = {
        'publish': 'PubSub'
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_pubsub_activity,
                          ns=Namespace.PUBSUB_EVENT,
                          priority=16),
        ]

    def _process_pubsub_activity(self, _client, stanza, properties):
        if not properties.is_pubsub_event:
            return

        if properties.pubsub_event.node != Namespace.ACTIVITY:
            return

        item = properties.pubsub_event.item
        if item is None:
            # Retract, Deleted or Purged
            return

        activity_node = item.getTag('activity', namespace=Namespace.ACTIVITY)
        if not activity_node.getChildren():
            self._log.info('Received activity: %s - no activity set',
                           properties.jid)
            return

        activity, subactivity, text = None, None, None
        for child in activity_node.getChildren():
            name = child.getName()
            if name == 'text':
                text = child.getData()
            elif name in ACTIVITIES:
                activity = name
                subactivity = self._parse_sub_activity(child)

        if activity is None and activity_node.getChildren():
            self._log.warning('No valid activity value found')
            self._log.warning(stanza)
            raise NodeProcessed

        data = ActivityData(activity, subactivity, text)
        pubsub_event = properties.pubsub_event._replace(data=data)
        self._log.info('Received activity: %s - %s', properties.jid, data)

        properties.pubsub_event = pubsub_event

    @staticmethod
    def _parse_sub_activity(activity):
        sub_activities = ACTIVITIES[activity.getName()]
        for sub in activity.getChildren():
            if sub.getName() in sub_activities:
                return sub.getName()
        return None

    @iq_request_task
    def set_activity(self, data):
        task = yield

        item = Node('activity', {'xmlns': Namespace.ACTIVITY})
        if data is not None and data.activity:
            activity_node = item.addChild(data.activity)
            if data.subactivity:
                activity_node.addChild(data.subactivity)
            if data.text:
                item.addChild('text', payload=data.text)

        result = yield self.publish(Namespace.ACTIVITY, item, id_='current')

        yield finalize(task, result) class Tune(BaseModule):

    _depends = {
        'publish': 'PubSub'
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_pubsub_tune,
                          ns=Namespace.PUBSUB_EVENT,
                          priority=16),
        ]

    def _process_pubsub_tune(self, _client, _stanza, properties):
        if not properties.is_pubsub_event:
            return

        if properties.pubsub_event.node != Namespace.TUNE:
            return

        item = properties.pubsub_event.item
        if item is None:
            # Retract, Deleted or Purged
            return

        tune_node = item.getTag('tune', namespace=Namespace.TUNE)
        if not tune_node.getChildren():
            self._log.info('Received tune: %s - no tune set', properties.jid)
            return

        tune_dict = {}
        for attr in TUNE_DATA:
            tune_dict[attr] = tune_node.getTagData(attr)

        data = TuneData(**tune_dict)
        if data.artist is None and data.title is None:
            self._log.warning('Missing artist or title: %s %s',
                              data, properties.jid)
            return

        pubsub_event = properties.pubsub_event._replace(data=data)
        self._log.info('Received tune: %s - %s', properties.jid, data)

        properties.pubsub_event = pubsub_event

    @iq_request_task
    def set_tune(self, data):
        task = yield

        item = Node('tune', {'xmlns': Namespace.TUNE})
        if data is not None:
            data = data._asdict()
            for tag, value in data.items():
                if value is not None:
                    item.addChild(tag, payload=value)

        result = yield self.publish(Namespace.TUNE, item, id_='current')

        yield finalize(task, result) class Mood(BaseModule):

    _depends = {
        'publish': 'PubSub'
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_pubsub_mood,
                          ns=Namespace.PUBSUB_EVENT,
                          priority=16),
        ]

    def _process_pubsub_mood(self, _client, stanza, properties):
        if not properties.is_pubsub_event:
            return

        if properties.pubsub_event.node != Namespace.MOOD:
            return

        item = properties.pubsub_event.item
        if item is None:
            # Retract, Deleted or Purged
            return

        mood_node = item.getTag('mood', namespace=Namespace.MOOD)
        if not mood_node.getChildren():
            self._log.info('Received mood: %s - removed mood', properties.jid)
            return

        mood, text = None, None
        for child in mood_node.getChildren():
            name = child.getName().strip()
            if name == 'text':
                text = child.getData()
            elif name in MOODS:
                mood = name

        if mood is None and mood_node.getChildren():
            self._log.warning('No valid mood value found')
            self._log.warning(stanza)
            raise NodeProcessed

        data = MoodData(mood, text)
        pubsub_event = properties.pubsub_event._replace(data=data)
        self._log.info('Received mood: %s - %s', properties.jid, data)

        properties.pubsub_event = pubsub_event

    @iq_request_task
    def set_mood(self, data):
        task = yield

        item = Node('mood', {'xmlns': Namespace.MOOD})
        if data is not None and data.mood:
            item.addChild(data.mood)

            if data.text:
                item.addChild('text', payload=data.text)

        result = yield self.publish(Namespace.MOOD, item, id_='current')

        yield finalize(task, result) class Location(BaseModule):

    _depends = {
        'publish': 'PubSub'
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_pubsub_location,
                          ns=Namespace.PUBSUB_EVENT,
                          priority=16),
        ]

    def _process_pubsub_location(self, _client, _stanza, properties):
        if not properties.is_pubsub_event:
            return

        if properties.pubsub_event.node != Namespace.LOCATION:
            return

        item = properties.pubsub_event.item
        if item is None:
            # Retract, Deleted or Purged
            return

        location_node = item.getTag('geoloc', namespace=Namespace.LOCATION)
        if not location_node.getChildren():
            self._log.info('Received location: %s - no location set',
                           properties.jid)
            return

        location_dict = {}
        for node in LOCATION_DATA:
            location_dict[node] = location_node.getTagData(node)
        data = LocationData(**location_dict)
        pubsub_event = properties.pubsub_event._replace(data=data)
        self._log.info('Received location: %s - %s', properties.jid, data)

        properties.pubsub_event = pubsub_event

    @iq_request_task
    def set_location(self, data):
        task = yield

        item = Node('geoloc', {'xmlns': Namespace.LOCATION})
        if data is not None:
            data = data._asdict()
            for tag, value in data.items():
                if value is not None:
                    item.addChild(tag, payload=value)

        result = yield self.publish(Namespace.LOCATION, item, id_='current')

        yield finalize(task, result) class UserAvatar(BaseModule):

    _depends = {
        'publish': 'PubSub',
        'request_item': 'PubSub',
        'request_items': 'PubSub',
        'purge': 'PubSub',
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_pubsub_avatar,
                          ns=Namespace.PUBSUB_EVENT,
                          priority=16),
        ]

    def _process_pubsub_avatar(self, _client, stanza, properties):
        if not properties.is_pubsub_event:
            return

        if properties.pubsub_event.node != Namespace.AVATAR_METADATA:
            return

        item = properties.pubsub_event.item
        if item is None:
            # Retract, Deleted or Purged
            return

        metadata = item.getTag('metadata', namespace=Namespace.AVATAR_METADATA)
        if metadata is None:
            self._log.warning('No metadata node found')
            self._log.warning(stanza)
            raise NodeProcessed

        if not metadata.getChildren():
            self._log.info('Received avatar metadata: %s - no avatar set',
                           properties.jid)
            return

        try:
            data = AvatarMetaData.from_node(metadata, item.getAttr('id'))
        except Exception as error:
            self._log.warning('Malformed user avatar data: %s', error)
            self._log.warning(stanza)
            raise NodeProcessed

        pubsub_event = properties.pubsub_event._replace(data=data)
        self._log.info('Received avatar metadata: %s - %s',
                       properties.jid, data)

        properties.pubsub_event = pubsub_event

    @iq_request_task
    def request_avatar_data(self, id_, jid=None):
        task = yield

        item = yield self.request_item(Namespace.AVATAR_DATA,
                                       id_=id_,
                                       jid=jid)

        raise_if_error(item)

        if item is None:
            yield task.set_result(None)

        yield _get_avatar_data(item, id_)

    @iq_request_task
    def request_avatar_metadata(self, jid=None):
        task = yield

        items = yield self.request_items(Namespace.AVATAR_METADATA,
                                         max_items=1,
                                         jid=jid)

        raise_if_error(items)

        if not items:
            yield task.set_result(None)

        item = items[0]
        metadata = item.getTag('metadata', namespace=Namespace.AVATAR_METADATA)
        if metadata is None:
            raise MalformedStanzaError('metadata node missing', item)

        if not metadata.getChildren():
            yield task.set_result(None)

        yield AvatarMetaData.from_node(metadata, item.getAttr('id'))

    @iq_request_task
    def set_avatar(self, avatar, public=False):

        task = yield

        access_model = 'open' if public else 'presence'

        if avatar is None:
            result = yield self._publish_avatar_metadata(None, access_model)
            raise_if_error(result)

            result = yield self.purge(Namespace.AVATAR_DATA)
            yield finalize(task, result)

        result = yield self._publish_avatar(avatar, access_model)

        yield finalize(task, result)

    @iq_request_task
    def _publish_avatar(self, avatar, access_model):
        task = yield

        options = {
            'pubsub#persist_items': 'true',
            'pubsub#access_model': access_model,
        }

        for info, data in avatar.pubsub_avatar_info():
            item = _make_avatar_data_node(data)

            result = yield self.publish(Namespace.AVATAR_DATA,
                                        item,
                                        id_=info.id,
                                        options=options,
                                        force_node_options=True)

            raise_if_error(result)

        result = yield self._publish_avatar_metadata(avatar.metadata,
                                                     access_model)

        yield finalize(task, result)

    @iq_request_task
    def _publish_avatar_metadata(self, metadata, access_model):
        task = yield

        options = {
            'pubsub#persist_items': 'true',
            'pubsub#access_model': access_model,
        }

        if metadata is None:
            metadata = AvatarMetaData()

        result = yield self.publish(Namespace.AVATAR_METADATA,
                                    metadata.to_node(),
                                    id_=metadata.default,
                                    options=options,
                                    force_node_options=True)

        yield finalize(task, result)

    @iq_request_task
    def set_access_model(self, public):
        task = yield

        access_model = 'open' if public else 'presence'

        result = yield self._client.get_module('PubSub').set_access_model(
            Namespace.AVATAR_DATA, access_model)

        raise_if_error(result)

        result = yield self._client.get_module('PubSub').set_access_model(
            Namespace.AVATAR_METADATA, access_model)

        yield finalize(task, result) class PrivateBookmarks(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = []

    @iq_request_task
    def request_bookmarks(self):
        _task = yield

        response = yield get_private_request()
        if response.isError():
            raise StanzaError(response)

        bookmarks = parse_private_bookmarks(response, self._log)
        for bookmark in bookmarks:
            self._log.info(bookmark)

        yield bookmarks

    @iq_request_task
    def store_bookmarks(self, bookmarks):
        _task = yield

        self._log.info('Store Bookmarks')

        storage_node = build_storage_node(bookmarks)
        response = yield Iq('set', Namespace.PRIVATE, payload=storage_node)
        yield process_response(response) class PEPBookmarks(BaseModule):

    _depends = {
        'publish': 'PubSub',
        'request_items': 'PubSub',
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_pubsub_bookmarks,
                          ns=Namespace.PUBSUB_EVENT,
                          priority=16),
        ]

    def _process_pubsub_bookmarks(self, _client, stanza, properties):
        if not properties.is_pubsub_event:
            return

        if properties.pubsub_event.node != Namespace.BOOKMARKS:
            return

        item = properties.pubsub_event.item
        if item is None:
            # Retract, Deleted or Purged
            return

        try:
            bookmarks = parse_bookmarks(item, self._log)
        except MalformedStanzaError as error:
            self._log.warning(error)
            self._log.warning(stanza)
            raise NodeProcessed

        if not bookmarks:
            self._log.info('Bookmarks removed')
            return

        pubsub_event = properties.pubsub_event._replace(data=bookmarks)
        self._log.info('Received bookmarks from: %s', properties.jid)
        for bookmark in bookmarks:
            self._log.info(bookmark)

        properties.pubsub_event = pubsub_event

    @iq_request_task
    def request_bookmarks(self):
        _task = yield

        items = yield self.request_items(Namespace.BOOKMARKS, max_items=1)
        raise_if_error(items)

        if not items:
            yield []

        bookmarks = parse_bookmarks(items[0], self._log)
        for bookmark in bookmarks:
            self._log.info(bookmark)

        yield bookmarks

    @iq_request_task
    def store_bookmarks(self, bookmarks):
        _task = yield

        self._log.info('Store Bookmarks')

        self.publish(Namespace.BOOKMARKS,
                     build_storage_node(bookmarks),
                     id_='current',
                     options=BOOKMARK_OPTIONS,
                     force_node_options=True) class NativeBookmarks(BaseModule):

    _depends = {
        'retract': 'PubSub',
        'publish': 'PubSub',
        'request_items': 'PubSub',
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_pubsub_bookmarks,
                          ns=Namespace.PUBSUB_EVENT,
                          priority=16),
        ]

    def _process_pubsub_bookmarks(self, _client, _stanza, properties):
        if not properties.is_pubsub_event:
            return

        if properties.pubsub_event.node != Namespace.BOOKMARKS_1:
            return

        item = properties.pubsub_event.item
        if item is None:
            # Retract, Deleted or Purged
            return

        try:
            bookmark_item = parse_bookmark(item)
        except MalformedStanzaError as error:
            self._log.warning(error)
            self._log.warning(error.stanza)
            raise NodeProcessed

        pubsub_event = properties.pubsub_event._replace(data=bookmark_item)
        self._log.info('Received bookmark item from: %s', properties.jid)
        self._log.info(bookmark_item)

        properties.pubsub_event = pubsub_event

    @iq_request_task
    def request_bookmarks(self):
        _task = yield

        items = yield self.request_items(Namespace.BOOKMARKS_1)
        raise_if_error(items)

        bookmarks = []
        for item in items:
            try:
                bookmark_item = parse_bookmark(item)
            except MalformedStanzaError as error:
                self._log.warning(error)
                self._log.warning(error.stanza)
                continue

            bookmarks.append(bookmark_item)

        for bookmark in bookmarks:
            self._log.info(bookmark)

        yield bookmarks

    @iq_request_task
    def retract_bookmark(self, bookmark_jid: JID):
        task = yield

        self._log.info('Retract Bookmark: %s', bookmark_jid)

        result = yield self.retract(Namespace.BOOKMARKS_1, str(bookmark_jid))
        yield finalize(task, result)

    @iq_request_task
    def store_bookmarks(self, bookmarks: list[BookmarkData]):
        _task = yield

        self._log.info('Store Bookmarks')

        for bookmark in bookmarks:
            self.publish(Namespace.BOOKMARKS_1,
                         build_conference_node(bookmark),
                         id_=str(bookmark.jid),
                         options=BOOKMARK_OPTIONS,
                         force_node_options=True)

        yield True class OpenPGP(BaseModule):

    _depends = {
        'publish': 'PubSub',
        'request_items': 'PubSub',
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_pubsub_openpgp,
                          ns=Namespace.PUBSUB_EVENT,
                          priority=16),
            StanzaHandler(name='message',
                          callback=self._process_openpgp_message,
                          ns=Namespace.OPENPGP,
                          priority=7),
        ]

    def _process_openpgp_message(self, _client, stanza, properties):
        openpgp = stanza.getTag('openpgp', namespace=Namespace.OPENPGP)
        if openpgp is None:
            self._log.warning('No openpgp node found')
            self._log.warning(stanza)
            return

        data = openpgp.getData()
        if not data:
            self._log.warning('No data in openpgp node found')
            self._log.warning(stanza)
            return

        self._log.info('Encrypted message received')
        try:
            properties.openpgp = b64decode(data)
        except Exception:
            self._log.warning('b64decode failed')
            self._log.warning(stanza)
            return

    def _process_pubsub_openpgp(self, _client, stanza, properties):
        """
        <item>
            <public-keys-list xmlns='urn:xmpp:openpgp:0'>
              <pubkey-metadata
                v4-fingerprint='1357B01865B2503C18453D208CAC2A9678548E35'
                date='2018-03-01T15:26:12Z'
                />
              <pubkey-metadata
                v4-fingerprint='67819B343B2AB70DED9320872C6464AF2A8E4C02'
                date='1953-05-16T12:00:00Z'
                />
            </public-keys-list>
        </item>
        """

        if not properties.is_pubsub_event:
            return

        if properties.pubsub_event.node != Namespace.OPENPGP_PK:
            return

        item = properties.pubsub_event.item
        if item is None:
            # Retract, Deleted or Purged
            return

        try:
            data = _parse_keylist(properties.jid, item)
        except ValueError as error:
            self._log.warning(error)
            self._log.warning(stanza)
            raise NodeProcessed

        if data is None:
            self._log.info('Received PGP keylist: %s - no keys set',
                           properties.jid)
            return

        pubsub_event = properties.pubsub_event._replace(data=data)
        self._log.info('Received PGP keylist: %s - %s', properties.jid, data)

        properties.pubsub_event = pubsub_event

    @iq_request_task
    def set_keylist(self, keylist, public=True):
        task = yield

        access_model = 'open' if public else 'presence'

        options = {
            'pubsub#persist_items': 'true',
            'pubsub#access_model': access_model,
        }

        result = yield self.publish(Namespace.OPENPGP_PK,
                                    _make_keylist(keylist),
                                    id_='current',
                                    options=options,
                                    force_node_options=True)

        yield finalize(task, result)

    @iq_request_task
    def set_public_key(self, key, fingerprint, date, public=True):
        task = yield

        access_model = 'open' if public else 'presence'

        options = {
            'pubsub#persist_items': 'true',
            'pubsub#access_model': access_model,
        }

        result = yield self.publish(f'{Namespace.OPENPGP_PK}:{fingerprint}',
                                    _make_public_key(key, date),
                                    id_='current',
                                    options=options,
                                    force_node_options=True)

        yield finalize(task, result)

    @iq_request_task
    def request_public_key(self, jid, fingerprint):
        task = yield

        items = yield self.request_items(
            f'{Namespace.OPENPGP_PK}:{fingerprint}',
            max_items=1,
            jid=jid)

        raise_if_error(items)

        if not items:
            yield task.set_result(None)

        try:
            key = _parse_public_key(jid, items[0])
        except ValueError as error:
            raise MalformedStanzaError(str(error), items)

        yield key

    @iq_request_task
    def request_keylist(self, jid=None):
        task = yield

        items = yield self.request_items(
            Namespace.OPENPGP_PK,
            max_items=1,
            jid=jid)

        raise_if_error(items)

        if not items:
            yield task.set_result(None)

        try:
            keylist = _parse_keylist(jid, items[0])
        except ValueError as error:
            raise MalformedStanzaError(str(error), items)

        self._log.info('Received keylist: %s', keylist)
        yield keylist

    @iq_request_task
    def request_secret_key(self):
        task = yield

        items = yield self.request_items(
            Namespace.OPENPGP_SK,
            max_items=1)

        raise_if_error(items)

        if not items:
            yield task.set_result(None)

        try:
            secret_key = _parse_secret_key(items[0])
        except ValueError as error:
            raise MalformedStanzaError(str(error), items)

        yield secret_key

    @iq_request_task
    def set_secret_key(self, secret_key):
        task = yield

        options = {
            'pubsub#persist_items': 'true',
            'pubsub#access_model': 'whitelist',
        }

        self._log.info('Set secret key')

        result = yield self.publish(Namespace.OPENPGP_SK,
                                    _make_secret_key(secret_key),
                                    id_='current',
                                    options=options,
                                    force_node_options=True)

        yield finalize(task, result) class OMEMO(BaseModule):

    _depends = {
        'publish': 'PubSub',
        'request_items': 'PubSub',
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_omemo_devicelist,
                          ns=Namespace.PUBSUB_EVENT,
                          priority=16),
            StanzaHandler(name='message',
                          callback=self._process_omemo_message,
                          ns=Namespace.OMEMO_TEMP,
                          priority=7),
        ]

    def _process_omemo_message(self, _client, stanza, properties):
        try:
            properties.omemo = _parse_omemo_message(stanza)
            self._log.info('Received message')
        except MalformedStanzaError as error:
            self._log.warning(error)
            self._log.warning(stanza)
            return

    def _process_omemo_devicelist(self, _client, stanza, properties):
        if not properties.is_pubsub_event:
            return

        if properties.pubsub_event.node != Namespace.OMEMO_TEMP_DL:
            return

        item = properties.pubsub_event.item
        if item is None:
            # Retract, Deleted or Purged
            return

        try:
            devices = _parse_devicelist(item)
        except MalformedStanzaError as error:
            self._log.warning(error)
            self._log.warning(stanza)
            raise NodeProcessed

        if not devices:
            self._log.info('Received OMEMO devicelist: %s - no devices set',
                           properties.jid)
            return

        pubsub_event = properties.pubsub_event._replace(data=devices)
        self._log.info('Received OMEMO devicelist: %s - %s',
                       properties.jid, devices)

        properties.pubsub_event = pubsub_event

    @iq_request_task
    def set_devicelist(self, devicelist=None, public=True):
        task = yield

        access_model = 'open' if public else 'presence'

        options = {
            'pubsub#persist_items': 'true',
            'pubsub#access_model': access_model,
        }

        result = yield self.publish(Namespace.OMEMO_TEMP_DL,
                                    _make_devicelist(devicelist),
                                    id_='current',
                                    options=options,
                                    force_node_options=True)

        yield finalize(task, result)

    @iq_request_task
    def request_devicelist(self, jid=None):
        task = yield

        items = yield self.request_items(Namespace.OMEMO_TEMP_DL,
                                         max_items=1,
                                         jid=jid)

        raise_if_error(items)

        if not items:
            yield task.set_result(None)

        yield _parse_devicelist(items[0])

    @iq_request_task
    def set_bundle(self, bundle, device_id, public=True):
        task = yield

        access_model = 'open' if public else 'presence'

        options = {
            'pubsub#persist_items': 'true',
            'pubsub#access_model': access_model,
        }

        result = yield self.publish(
            f'{Namespace.OMEMO_TEMP_BUNDLE}:{device_id}',
            _make_bundle(bundle),
            id_='current',
            options=options,
            force_node_options=True)

        yield finalize(task, result)

    @iq_request_task
    def request_bundle(self, jid, device_id):
        task = yield

        items = yield self.request_items(
            f'{Namespace.OMEMO_TEMP_BUNDLE}:{device_id}',
            max_items=1,
            jid=jid)

        raise_if_error(items)

        if not items:
            yield task.set_result(None)

        yield _parse_bundle(items[0]) class Annotations(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = []

    @property
    def domain(self):
        return self._client.get_bound_jid().domain

    @iq_request_task
    def request_annotations(self):
        _task = yield

        response = yield _make_request()
        if response.isError():
            raise StanzaError(response)

        query = response.getQuery()
        storage = query.getTag('storage', namespace=Namespace.ROSTERNOTES)
        if storage is None:
            raise MalformedStanzaError('storage node missing', response)

        notes = []
        for note in storage.getTags('note'):
            try:
                jid = JID.from_string(note.getAttr('jid'))
            except Exception as error:
                self._log.warning('Invalid JID: %s, %s',
                                  note.getAttr('jid'), error)
                continue

            cdate = note.getAttr('cdate')
            if cdate is not None:
                cdate = parse_datetime(cdate, epoch=True)

            mdate = note.getAttr('mdate')
            if mdate is not None:
                mdate = parse_datetime(mdate, epoch=True)

            data = note.getData()
            notes.append(AnnotationNote(jid=jid, cdate=cdate,
                                        mdate=mdate, data=data))

        self._log.info('Received annotations from %s:', self.domain)
        for note in notes:
            self._log.info(note)
        yield notes

    @iq_request_task
    def set_annotations(self, notes):
        _task = yield

        for note in notes:
            self._log.info(note)

        response = yield _make_set_request(notes)
        yield process_response(response) class Muclumbus(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = []

        self._proxy_resolver = None
        self._soup_session = Soup.Session()

    def set_proxy(self, proxy):
        if proxy is None:
            return
        self._proxy_resolver = proxy.get_resolver()
        self._soup_session.props.proxy_resolver = self._proxy_resolver

    @iq_request_task
    def request_parameters(self, jid):
        task = yield

        response = yield _make_parameter_request(jid)
        if response.isError():
            raise StanzaError(response)

        search = response.getTag('search', namespace=Namespace.MUCLUMBUS)
        if search is None:
            raise MalformedStanzaError('search node missing', response)

        dataform = search.getTag('x', namespace=Namespace.DATA)
        if dataform is None:
            raise MalformedStanzaError('dataform node missing', response)

        self._log.info('Muclumbus parameters received')
        yield finalize(task, extend_form(node=dataform))

    @iq_request_task
    def set_search(self, jid, dataform, items_per_page=50, after=None):
        _task = yield

        response = yield _make_search_query(jid,
                                            dataform,
                                            items_per_page,
                                            after)
        if response.isError():
            raise StanzaError(response)

        result = response.getTag('result', namespace=Namespace.MUCLUMBUS)
        if result is None:
            raise MalformedStanzaError('result node missing', response)

        items = result.getTags('item')
        if not items:
            yield MuclumbusResult(first=None,
                                  last=None,
                                  max=None,
                                  end=True,
                                  items=[])

        set_ = result.getTag('set', namespace=Namespace.RSM)
        if set_ is None:
            raise MalformedStanzaError('set node missing', response)

        first = set_.getTagData('first')
        last = set_.getTagData('last')
        try:
            max_ = int(set_.getTagData('max'))
        except Exception:
            raise MalformedStanzaError('invalid max value', response)

        results = []
        for item in items:
            jid = item.getAttr('address')
            name = item.getTagData('name')
            nusers = item.getTagData('nusers')
            description = item.getTagData('description')
            language = item.getTagData('language')
            is_open = item.getTag('is-open') is not None

            try:
                anonymity_mode = AnonymityMode(
                    item.getTagData('anonymity-mode'))
            except ValueError:
                anonymity_mode = AnonymityMode.UNKNOWN
            results.append(MuclumbusItem(jid=jid,
                                         name=name or '',
                                         nusers=nusers or '',
                                         description=description or '',
                                         language=language or '',
                                         is_open=is_open,
                                         anonymity_mode=anonymity_mode))
        yield MuclumbusResult(first=first,
                              last=last,
                              max=max_,
                              end=len(items) < max_,
                              items=results)

    @http_request_task
    def set_http_search(self, uri, keywords, after=None):
        _task = yield

        search = {'keywords': keywords}
        if after is not None:
            search['after'] = after

        message = Soup.Message.new('POST', uri)
        message.set_request('application/json',
                            Soup.MemoryUse.COPY,
                            json.dumps(search).encode())

        response_message = yield message

        soup_body = response_message.get_property('response-body')

        if response_message.status_code != 200:
            self._log.warning(soup_body.data)
            yield MuclumbusResult(first=None,
                                  last=None,
                                  max=None,
                                  end=True,
                                  items=[])

        response = json.loads(soup_body.data)

        result = response['result']
        items = result.get('items')
        if items is None:
            yield MuclumbusResult(first=None,
                                  last=None,
                                  max=None,
                                  end=True,
                                  items=[])

        results = []
        for item in items:
            try:
                anonymity_mode = AnonymityMode(item['anonymity_mode'])
            except (ValueError, KeyError):
                anonymity_mode = AnonymityMode.UNKNOWN

            results.append(
                MuclumbusItem(jid=item['address'],
                              name=item['name'] or '',
                              nusers=str(item['nusers'] or ''),
                              description=item['description'] or '',
                              language=item['language'] or '',
                              is_open=item['is_open'],
                              anonymity_mode=anonymity_mode))

        yield MuclumbusResult(first=None,
                              last=result['last'],
                              max=None,
                              end=not result['more'],
                              items=results) class SoftwareVersion(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='iq',
                          callback=self._answer_request,
                          typ='get',
                          priority=60,
                          ns=Namespace.VERSION),
        ]

        self._name = None
        self._version = None
        self._os = None

        self._enabled = False
        self._allow_reply_func = None

    def disable(self):
        self._enabled = False

    def set_allow_reply_func(self, func):
        self._allow_reply_func = func

    @iq_request_task
    def request_software_version(self, jid):
        _task = yield

        response = yield Iq(typ='get', to=jid, queryNS=Namespace.VERSION)
        if response.isError():
            raise StanzaError(response)

        yield _parse_info(response)

    def set_software_version(self, name, version, os=None):
        self._name, self._version, self._os = name, version, os
        self._enabled = True

    def _answer_request(self, _con, stanza, _properties):
        self._log.info('Request received from %s', stanza.getFrom())
        if (not self._enabled or
                self._name is None or
                self._version is None):
            self._client.send_stanza(Error(stanza, ERR_SERVICE_UNAVAILABLE))
            raise NodeProcessed

        if self._allow_reply_func is not None:
            if not self._allow_reply_func(stanza.getFrom()):
                self._client.send_stanza(Error(stanza, ERR_FORBIDDEN))
                raise NodeProcessed

        iq = stanza.buildReply('result')
        query = iq.getQuery()
        query.setTagData('name', self._name)
        query.setTagData('version', self._version)
        if self._os is not None:
            query.setTagData('os', self._os)
        self._log.info('Send software version: %s %s %s',
                       self._name, self._version, self._os)

        self._client.send_stanza(iq)
        raise NodeProcessed class AdHoc(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = []

    @iq_request_task
    def request_command_list(self, jid=None):
        _task = yield

        if jid is None:
            jid = self._client.get_bound_jid().bare
        response = yield get_disco_request(Namespace.DISCO_ITEMS,
                                           jid,
                                           node=Namespace.COMMANDS)
        if response.isError():
            raise StanzaError(response)

        children = response.getQueryChildren()
        if not children:
            raise MalformedStanzaError('query children missing', response)

        command_list = []
        for item in children:
            if item.getName() != 'item':
                continue
            try:
                command_list.append(AdHocCommand(**item.getAttrs()))
            except Exception as error:
                raise MalformedStanzaError(f'invalid item attributes: {error}',
                                           response)

        yield command_list

    @iq_request_task
    def execute_command(self, cmd, action=None, dataform=None):
        _task = yield

        if action is None:
            action = AdHocAction.EXECUTE
        attrs = {'node': cmd.node,
                 'xmlns': Namespace.COMMANDS,
                 'action': action.value}
        if cmd.sessionid is not None:
            attrs['sessionid'] = cmd.sessionid

        response = yield _make_command(cmd, attrs, dataform)
        if response.isError():
            raise StanzaError(response)

        command = response.getTag('command', namespace=Namespace.COMMANDS)
        if command is None:
            raise MalformedStanzaError('command node missing', response)

        node = command.getAttr('node')
        if node is None:
            raise MalformedStanzaError('node attribute missing', response)

        status = command.getAttr('status')
        if status is None:
            raise MalformedStanzaError('status attribute missing', response)

        if status not in ('executing', 'completed', 'canceled'):
            raise MalformedStanzaError('invalid status attribute %s' % status,
                                       response)

        status = AdHocStatus(status)

        sessionid = command.getAttr('sessionid')
        if sessionid is None and _expect_sessionid(status, cmd.sessionid):
            raise MalformedStanzaError('sessionid attribute missing', response)

        try:
            notes = _parse_notes(command)
        except ValueError as error:
            raise MalformedStanzaError(error, response)

        try:
            actions, default = _parse_actions(command)
        except ValueError as error:
            raise MalformedStanzaError(error, response)

        yield AdHocCommand(
            jid=response.getFrom(),
            name=None,
            node=node,
            sessionid=sessionid,
            status=status,
            data=command.getTag('x', namespace=Namespace.DATA),
            actions=actions,
            default=default,
            notes=notes) class IBB(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='iq',
                          callback=self._process_ibb,
                          ns=Namespace.IBB,
                          priority=20),
        ]

    def _process_ibb(self, _client, stanza, properties):
        if properties.type.is_set:
            open_ = stanza.getTag('open', namespace=Namespace.IBB)
            if open_ is not None:
                properties.ibb = self._parse_open(stanza, open_)
                return

            close = stanza.getTag('close', namespace=Namespace.IBB)
            if close is not None:
                properties.ibb = self._parse_close(stanza, close)
                return

            data = stanza.getTag('data', namespace=Namespace.IBB)
            if data is not None:
                properties.ibb = self._parse_data(stanza, data)
                return

    def _parse_open(self, stanza, open_):
        attrs = open_.getAttrs()
        try:
            block_size = int(attrs.get('block-size'))
        except Exception as error:
            self._log.warning(error)
            self._log.warning(stanza)
            self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
            raise NodeProcessed

        if block_size > 65535:
            self._log.warning('Invalid block-size')
            self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
            raise NodeProcessed

        sid = attrs.get('sid')
        if not sid:
            self._log.warning('Invalid sid')
            self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
            raise NodeProcessed

        type_ = attrs.get('stanza')
        if type_ == 'message':
            self._client.send_stanza(ErrorStanza(stanza,
                                                 ERR_FEATURE_NOT_IMPLEMENTED))
            raise NodeProcessed

        return IBBData(type='open', block_size=block_size, sid=sid)

    def _parse_close(self, stanza, close):
        sid = close.getAttrs().get('sid')
        if sid is None:
            self._log.warning('Invalid sid')
            self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
            raise NodeProcessed
        return IBBData(type='close', sid=sid)

    def _parse_data(self, stanza, data):
        attrs = data.getAttrs()

        sid = attrs.get('sid')
        if sid is None:
            self._log.warning('Invalid sid')
            self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
            raise NodeProcessed

        try:
            seq = int(attrs.get('seq'))
        except Exception:
            self._log.exception('Invalid seq')
            self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
            raise NodeProcessed

        try:
            decoded_data = b64decode(data.getData())
        except Exception:
            self._log.exception('Failed to decode IBB data')
            self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
            raise NodeProcessed

        return IBBData(type='data', sid=sid, seq=seq, data=decoded_data)

    def send_reply(self, stanza, error=None):
        if error is None:
            reply = stanza.buildReply('result')
            reply.getChildren().clear()
        else:
            reply = ErrorStanza(stanza, error)
        self._client.send_stanza(reply)

    @iq_request_task
    def send_open(self, jid, sid, block_size):
        _task = yield

        response = yield _make_ibb_open(jid, sid, block_size)
        yield process_response(response)

    @iq_request_task
    def send_close(self, jid, sid):
        _task = yield

        response = yield _make_ibb_close(jid, sid)
        yield process_response(response)

    @iq_request_task
    def send_data(self, jid, sid, seq, data):
        _task = yield

        response = yield _make_ibb_data(jid, sid, seq, data)
        yield process_response(response) class Discovery(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='iq',
                          callback=self._process_disco_info,
                          typ='get',
                          ns=Namespace.DISCO_INFO,
                          priority=90),
        ]

    @staticmethod
    def _process_disco_info(client, stanza, properties):
        iq = stanza.buildReply('error')
        iq.addChild(node=ErrorNode(ERR_ITEM_NOT_FOUND))
        client.send_stanza(iq)
        raise NodeProcessed

    @iq_request_task
    def disco_info(self, jid, node=None):
        _task = yield

        response = yield get_disco_request(Namespace.DISCO_INFO, jid, node)
        if response.isError():
            raise StanzaError(response)
        yield parse_disco_info(response)

    @iq_request_task
    def disco_items(self, jid, node=None):
        _task = yield

        response = yield get_disco_request(Namespace.DISCO_ITEMS, jid, node)
        if response.isError():
            raise StanzaError(response)
        yield parse_disco_items(response) class ChatMarkers(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_message_marker,
                          ns=Namespace.CHATMARKERS,
                          priority=15),
        ]

    def _process_message_marker(self, _client, stanza, properties):
        type_ = stanza.getTag('received', namespace=Namespace.CHATMARKERS)
        if type_ is None:
            type_ = stanza.getTag('displayed', namespace=Namespace.CHATMARKERS)
            if type_ is None:
                type_ = stanza.getTag('acknowledged',
                                      namespace=Namespace.CHATMARKERS)
                if type_ is None:
                    return

        name = type_.getName()
        id_ = type_.getAttr('id')
        if id_ is None:
            self._log.warning('Chatmarker without id')
            self._log.warning(stanza)
            return

        properties.marker = ChatMarker(name, id_) class Receipts(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_message_receipt,
                          ns=Namespace.RECEIPTS,
                          priority=15),
        ]

    def _process_message_receipt(self, _client, stanza, properties):
        request = stanza.getTag('request', namespace=Namespace.RECEIPTS)
        if request is not None:
            properties.receipt = ReceiptData(request.getName())
            return

        received = stanza.getTag('received', namespace=Namespace.RECEIPTS)
        if received is not None:
            id_ = received.getAttr('id')
            if id_ is None:
                self._log.warning('Receipt without id attr')
                self._log.warning(stanza)
                return

            properties.receipt = ReceiptData(received.getName(), id_) class OOB(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_message_oob,
                          ns=Namespace.X_OOB,
                          priority=15),
        ]

    def _process_message_oob(self, _client, stanza, properties):
        oob = stanza.getTag('x', namespace=Namespace.X_OOB)
        if oob is None:
            return

        url = oob.getTagData('url')
        if url is None:
            self._log.warning('OOB data without url')
            self._log.warning(stanza)
            return

        desc = oob.getTagData('desc')
        properties.oob = OOBData(url, desc) class Correction(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_message_correction,
                          ns=Namespace.CORRECT,
                          priority=15),
        ]

    def _process_message_correction(self, _client, stanza, properties):
        replace = stanza.getTag('replace', namespace=Namespace.CORRECT)
        if replace is None:
            return

        id_ = replace.getAttr('id')
        if id_ is None:
            self._log.warning('Correcton without id attribute')
            self._log.warning(stanza)
            return

        if stanza.getID() == id_:
            self._log.warning('correcton id == message id')
            self._log.warning(stanza)
            return

        properties.correction = CorrectionData(id_) class Attention(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_message_attention,
                          ns=Namespace.ATTENTION,
                          priority=15),
        ]

    def _process_message_attention(self, _client, stanza, properties):
        attention = stanza.getTag('attention', namespace=Namespace.ATTENTION)
        if attention is None:
            return

        if properties.is_mam_message:
            return

        if properties.is_carbon_message and properties.carbon.is_sent:
            return

        if stanza.getTag('delay', namespace=Namespace.DELAY2) is not None:
            return

        properties.attention = True class SecurityLabels(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_message_security_label,
                          ns=Namespace.SECLABEL,
                          priority=15),
        ]

    def _process_message_security_label(self, _client, stanza, properties):
        security = stanza.getTag('securitylabel', namespace=Namespace.SECLABEL)
        if security is None:
            return

        try:
            security_label = SecurityLabel.from_node(security)
        except ValueError as error:
            self._log.warning(error)
            return

        properties.security_label = security_label

    @iq_request_task
    def request_catalog(self, jid):
        _task = yield

        response = yield _make_catalog_request(self._client.domain, jid)
        if response.isError():
            raise StanzaError(response)

        catalog_node = response.getTag('catalog',
                                       namespace=Namespace.SECLABEL_CATALOG)
        to = catalog_node.getAttr('to')
        items = catalog_node.getTags('item')

        labels = {}
        default = None
        for item in items:
            label = item.getAttr('selector')
            if label is None:
                continue

            security = item.getTag('securitylabel',
                                   namespace=Namespace.SECLABEL)
            if security is None:
                continue

            try:
                security_label = SecurityLabel.from_node(security)
            except ValueError:
                continue

            labels[label] = security_label

            if item.getAttr('default') == 'true':
                default = label

        yield Catalog(labels=labels, default=default) class Chatstates(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self._process_message_chatstate,
                          ns=Namespace.CHATSTATES,
                          priority=15),
        ]

    def _process_message_chatstate(self, _client, stanza, properties):
        try:
            chatstate = parse_chatstate(stanza)
        except ValueError as error:
            self._log.warning('Invalid chatstate: %s', error)
            self._log.warning(stanza)
            return

        if chatstate is None:
            return

        if properties.is_mam_message:
            return

        if stanza.getTag('delay', namespace=Namespace.DELAY2) is not None:
            return

        properties.chatstate = chatstate class Register(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = []

    @iq_request_task
    def unregister(self, jid=None):
        _task = yield

        response = yield _make_unregister_request(jid)
        yield process_response(response)

    @iq_request_task
    def request_register_form(self, jid=None):
        _task = yield

        if jid is None:
            jid = self._client.domain

        response = yield Iq('get', Namespace.REGISTER, to=jid)
        if response.isError():
            raise StanzaError(response)

        yield _parse_register_data(response)

    @iq_request_task
    def submit_register_form(self, form, jid=None):
        _task = yield

        if jid is None:
            jid = self._client.domain

        response = yield _make_register_form(jid, form)
        if not response.isError():
            yield process_response(response)

        else:
            data = _parse_register_data(response)
            raise RegisterStanzaError(response, data)

    @iq_request_task
    def change_password(self, password):
        _task = yield

        response = yield _make_password_change_request(
            self._client.domain, self._client.username, password)
        if not response.isError():
            yield process_response(response)

        else:
            query = response.getQuery()
            if query is None:
                raise StanzaError(response)

            form = get_form(query, 'jabber:iq:register:changepassword')
            if form is None or response.getType() != 'modify':
                raise StanzaError(response)

            raise ChangePasswordStanzaError(response, form)

    @iq_request_task
    def change_password_with_form(self, form):
        _task = yield

        response = yield _make_password_change_with_form(self._client.domain,
                                                         form)
        yield process_response(response) class HTTPUpload(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = []

    @iq_request_task
    def request_slot(self, jid, filename, size, content_type):
        _task = yield

        response = yield _make_request(jid, filename, size, content_type)
        if response.isError():
            raise HTTPUploadStanzaError(response)

        slot = response.getTag('slot', namespace=Namespace.HTTPUPLOAD_0)
        if slot is None:
            raise MalformedStanzaError('slot node missing', response)

        put_uri = slot.getTagAttr('put', 'url')
        if put_uri is None:
            raise MalformedStanzaError('put uri missing', response)

        get_uri = slot.getTagAttr('get', 'url')
        if get_uri is None:
            raise MalformedStanzaError('get uri missing', response)

        headers = {}
        for header in slot.getTag('put').getTags('header'):
            name = header.getAttr('name')
            if name not in ALLOWED_HEADERS:
                raise MalformedStanzaError(
                    'not allowed header found: %s' % name, response)

            data = header.getData()
            if '\n' in data:
                raise MalformedStanzaError(
                    'newline in header data found', response)

            headers[name] = data

        yield HTTPUploadData(put_uri=put_uri,
                             get_uri=get_uri,
                             headers=headers) class MAM(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = []

    @iq_request_task
    def make_query(self,
                   jid,
                   queryid=None,
                   start=None,
                   end=None,
                   with_=None,
                   after=None,
                   max_=70):

        _task = yield

        response = yield _make_request(jid, queryid,
                                       start, end, with_, after, max_)
        if response.isError():
            raise StanzaError(response)

        jid = response.getFrom()
        fin = response.getTag('fin', namespace=Namespace.MAM_2)
        if fin is None:
            raise MalformedStanzaError('fin node missing', response)

        rsm = parse_rsm(fin)
        if rsm is None:
            raise MalformedStanzaError('rsm set missing', response)

        complete = fin.getAttr('complete') == 'true'
        if max_ != 0 and not complete:
            # max_ == 0 is a request for count of the items in a result set
            # in this case first and last will be absent
            # See: https://xmpp.org/extensions/xep-0059.html#count
            if rsm.first is None or rsm.last is None:
                raise MalformedStanzaError('first or last element missing',
                                           response)

        yield MAMQueryData(jid=jid,
                           complete=complete,
                           rsm=rsm)

    @iq_request_task
    def request_preferences(self):
        _task = yield

        response = yield _make_pref_request()
        if response.isError():
            raise StanzaError(response)

        prefs = response.getTag('prefs', namespace=Namespace.MAM_2)
        if prefs is None:
            raise MalformedStanzaError('prefs node missing', response)

        default = prefs.getAttr('default')
        if default is None:
            raise MalformedStanzaError('default attr missing', response)

        always_node = prefs.getTag('always')
        if always_node is None:
            raise MalformedStanzaError('always node missing', response)

        always = _get_preference_jids(always_node)

        never_node = prefs.getTag('never')
        if never_node is None:
            raise MalformedStanzaError('never node missing', response)

        never = _get_preference_jids(never_node)
        yield MAMPreferencesData(default=default,
                                 always=always,
                                 never=never)

    @iq_request_task
    def set_preferences(self, default, always, never):
        _task = yield

        if default not in ('always', 'never', 'roster'):
            raise ValueError('Wrong default preferences type')

        response = yield _make_set_pref_request(default, always, never)
        yield process_response(response) class VCardTemp(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = []

    @iq_request_task
    def request_vcard(self, jid=None):
        _task = yield

        response = yield _make_vcard_request(jid)

        if response.isError():
            raise StanzaError(response)

        vcard_node = _get_vcard_node(response)
        yield VCard.from_node(vcard_node)

    @iq_request_task
    def set_vcard(self, vcard, jid=None):
        _task = yield

        response = yield _make_vcard_publish(jid, vcard)
        yield process_response(response) class VCard4(BaseModule):

    _depends = {
        'publish': 'PubSub',
        'request_items': 'PubSub',
    }

    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = []

    @iq_request_task
    def request_vcard(self, jid=None):
        task = yield

        items = yield self.request_items(Namespace.VCARD4_PUBSUB,
                                         jid=jid,
                                         max_items=1)

        raise_if_error(items)

        if not items:
            yield task.set_result(None)

        yield _get_vcard(items[0])

    @iq_request_task
    def set_vcard(self, vcard, public=False):
        task = yield

        access_model = 'open' if public else 'presence'

        options = {
            'pubsub#persist_items': 'true',
            'pubsub#access_model': access_model,
        }

        result = yield self.publish(Namespace.VCARD4_PUBSUB,
                                    vcard.to_node(),
                                    id_='current',
                                    options=options,
                                    force_node_options=True)

        yield finalize(task, result) class Ping(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='iq',
                          callback=self._process_ping,
                          typ='get',
                          ns=Namespace.PING,
                          priority=15),
        ]

    def _process_ping(self, _client, stanza, properties):
        self._log.info('Send pong to %s', stanza.getFrom())
        iq = stanza.buildSimpleReply('result')
        self._client.send_stanza(iq)
        raise NodeProcessed

    @iq_request_task
    def ping(self, jid):
        _task = yield

        response = yield _make_ping_request(jid)
        yield process_response(response) class Delimiter(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = []

    @iq_request_task
    def request_delimiter(self):
        _task = yield

        response = yield _make_request()
        if response.isError():
            raise StanzaError(response)

        delimiter = response.getQuery().getTagData('roster') or None
        yield delimiter

    @iq_request_task
    def set_delimiter(self, delimiter):
        _task = yield

        response = yield _make_set_request(delimiter)
        yield process_response(response) class Roster(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='iq',
                          callback=self._process_roster_push,
                          typ='set',
                          priority=15,
                          ns=Namespace.ROSTER),
        ]

    @iq_request_task
    def request_roster(self, version=None):
        _task = yield

        ver_support = self._client.features.has_roster_version()
        if not ver_support:
            version = None

        if ver_support and version is None:
            version = ''

        self._log.info('Roster versioning supported: %s', ver_support)

        response = yield _make_request(version, ver_support)
        if response.isError():
            raise StanzaError(response)

        query = response.getTag('query', namespace=Namespace.ROSTER)
        if query is None:
            if not ver_support:
                raise MalformedStanzaError('query node missing', response)
            yield RosterData(None, version)

        pushed_items, version = self._parse_push(response, ver_support)
        yield RosterData(pushed_items, version)

    def _process_roster_push(self, _client, stanza, properties):
        from_ = stanza.getFrom()
        if from_ is not None:
            if not self._client.get_bound_jid().bare == from_:
                self._log.warning('Malicious Roster Push from %s', from_)
                raise NodeProcessed

        ver_support = self._client.features.has_roster_version()
        pushed_items, version = self._parse_push(stanza, ver_support)
        if len(pushed_items) != 1:
            self._log.warning('Roster push contains more than one item')
            self._log.warning(stanza)
            raise NodeProcessed

        item = pushed_items[0]
        properties.roster = RosterPush(item, version)

        self._log.info('Roster Push, version: %s', properties.roster.version)
        self._log.info(item)

        self._ack_roster_push(stanza)

    def _ack_roster_push(self, stanza):
        iq = Iq('result',
                to=stanza.getFrom(),
                frm=stanza.getTo(),
                attrs={'id': stanza.getID()})
        self._client.send_stanza(iq)

    @iq_request_task
    def delete_item(self, jid):
        _task = yield

        response = yield _make_delete(jid)
        yield process_response(response)

    @iq_request_task
    def set_item(self, jid, name, groups=None):
        _task = yield

        response = yield _make_set(jid, name, groups)
        yield process_response(response)

    def _parse_push(self, stanza, ver_support):
        query = stanza.getTag('query', namespace=Namespace.ROSTER)

        version = None
        if ver_support:
            version = query.getAttr('ver')
            if version is None:
                raise MalformedStanzaError('ver attribute missing', stanza)

        pushed_items = []
        for item in query.getTags('item'):
            try:
                roster_item = RosterItem.from_node(item)
            except Exception:
                self._log.warning('Invalid roster item')
                self._log.warning(stanza)
                continue

            pushed_items.append(roster_item)

        return pushed_items, version class LastActivity(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='iq',
                          callback=self._answer_request,
                          priority=60,
                          typ='get',
                          ns=Namespace.LAST),
        ]

        self._idle_func = None
        self._allow_reply_func = None

    def disable(self):
        self._idle_func = None

    def set_idle_func(self, func):
        self._idle_func = func

    def set_allow_reply_func(self, func):
        self._allow_reply_func = func

    @iq_request_task
    def request_last_activity(self, jid):
        _task = yield

        response = yield _make_request(jid)
        if response.isError():
            raise StanzaError(response)

        yield _parse_response(response)

    def _answer_request(self, _client, stanza, _properties):
        self._log.info('Request received from %s', stanza.getFrom())
        if self._idle_func is None:
            self._client.send_stanza(Error(stanza, ERR_SERVICE_UNAVAILABLE))
            raise NodeProcessed

        if self._allow_reply_func is not None:
            if not self._allow_reply_func(stanza.getFrom()):
                self._client.send_stanza(Error(stanza, ERR_FORBIDDEN))
                raise NodeProcessed

        seconds = self._idle_func()
        iq = stanza.buildReply('result')
        query = iq.getQuery()
        query.setAttr('seconds', seconds)
        self._log.info('Send last activity: %s', seconds)
        self._client.send_stanza(iq)
        raise NodeProcessed class EntityTime(BaseModule):
    def __init__(self, client):
        BaseModule.__init__(self, client)

        self._client = client
        self.handlers = [
            StanzaHandler(name='iq',
                          callback=self._answer_request,
                          priority=60,
                          typ='get',
                          ns=Namespace.TIME),
        ]

        self._enabled = False
        self._allow_reply_func = None

    def disable(self):
        self._enabled = False

    def enable(self):
        self._enabled = True

    def set_allow_reply_func(self, func):
        self._allow_reply_func = func

    @iq_request_task
    def request_entity_time(self, jid):
        _task = yield

        response = yield _make_request(jid)
        if response.isError():
            raise StanzaError(response)

        yield _parse_response(response)

    def _answer_request(self, _con, stanza, _properties):
        self._log.info('Request received from %s', stanza.getFrom())
        if not self._enabled:
            self._client.send_stanza(Error(stanza, ERR_SERVICE_UNAVAILABLE))
            raise NodeProcessed

        if self._allow_reply_func is not None:
            if not self._allow_reply_func(stanza.getFrom()):
                self._client.send_stanza(Error(stanza, ERR_FORBIDDEN))
                raise NodeProcessed

        time, tzo = get_local_time()
        iq = stanza.buildSimpleReply('result')
        time_node = iq.addChild('time', namespace=Namespace.TIME)
        time_node.setTagData('utc', time)
        time_node.setTagData('tzo', tzo)
        self._log.info('Send time: %s %s', time, tzo)
        self._client.send_stanza(iq)
        raise NodeProcessed def unwrap_carbon(stanza, own_jid):
    carbon = stanza.getTag('received', namespace=Namespace.CARBONS)
    if carbon is None:
        carbon = stanza.getTag('sent', namespace=Namespace.CARBONS)
        if carbon is None:
            return stanza, None

    # Carbon must be from our bare jid
    if stanza.getFrom() != own_jid.new_as_bare():
        raise InvalidFrom('Invalid from: %s' % stanza.getAttr('from'))

    forwarded = carbon.getTag('forwarded', namespace=Namespace.FORWARD)
    message = Message(node=forwarded.getTag('message'))

    type_ = carbon.getName()

    # Fill missing to/from
    to = message.getTo()
    if to is None:
        message.setTo(own_jid.bare)

    frm = message.getFrom()
    if frm is None:
        message.setFrom(own_jid.bare)

    if type_ == 'received':
        if message.getFrom().bare_match(own_jid):
            # Drop 'received' Carbons from ourself, we already
            # got the message with the 'sent' Carbon or via the
            # message itself
            raise NodeProcessed('Drop "received"-Carbon from ourself')

        if message.getTag('x', namespace=Namespace.MUC_USER) is not None:
            # A MUC broadcasts messages sent to us to all resources
            # there is no need to process the received carbon
            raise NodeProcessed('Drop MUC-PM "received"-Carbon')

    return message, CarbonData(type=type_) def unwrap_mam(stanza, own_jid):
    result = stanza.getTag('result', namespace=Namespace.MAM_2)
    if result is None:
        result = stanza.getTag('result', namespace=Namespace.MAM_1)
        if result is None:
            return stanza, None

    query_id = result.getAttr('queryid')
    if query_id is None:
        log.warning('No queryid on MAM message')
        log.warning(stanza)
        raise InvalidStanza

    id_ = result.getAttr('id')
    if id_ is None:
        log.warning('No id on MAM message')
        log.warning(stanza)
        raise InvalidStanza

    forwarded = result.getTag('forwarded', namespace=Namespace.FORWARD)
    message = Message(node=forwarded.getTag('message'))

    # Fill missing to/from
    to = message.getTo()
    if to is None:
        message.setTo(own_jid.bare)

    frm = message.getFrom()
    if frm is None:
        message.setFrom(own_jid.bare)

    # Timestamp parsing
    # Most servers dont set the 'from' attr, so we cant check for it
    delay_timestamp = parse_delay(forwarded)
    if delay_timestamp is None:
        log.warning('No timestamp on MAM message')
        log.warning(stanza)
        raise InvalidStanza

    return message, MAMData(id=id_,
                            query_id=query_id,
                            archive=stanza.getFrom(),
                            namespace=result.getNamespace(),
                            timestamp=delay_timestamp) def get_properties_struct(name, own_jid):
    if name == 'message':
        return MessageProperties(own_jid)
    if name == 'iq':
        return IqProperties(own_jid)
    if name == 'presence':
        return PresenceProperties(own_jid)
    return Properties() def get_invalid_xml_regex():
    # \ufddo -> \ufdef range
    c = '\ufdd0'
    r = c
    while c < '\ufdef':
        c = chr(ord(c) + 1)
        r += '|' + c

    # \ufffe-\uffff, \u1fffe-\u1ffff, ..., \u10fffe-\u10ffff
    c = '\ufffe'
    r += '|' + c
    r += '|' + chr(ord(c) + 1)
    while c < '\U0010fffe':
        c = chr(ord(c) + 0x10000)
        r += '|' + c
        r += '|' + chr(ord(c) + 1)

    return re.compile(r) def is_websocket_close(stanza: Node) -> bool:
    return (stanza.getName() == 'close' and
            stanza.getNamespace() == Namespace.FRAMING) def is_websocket_stream_error(stanza: Node) -> bool:
    return (stanza.getName() == 'error' and
            stanza.getNamespace() == Namespace.STREAMS) class Observable:
    def __init__(self, log_: logging.Logger):
        self._log = log_
        self._frozen = False
        self._callbacks: defaultdict[str, list[Callable[..., Any]]] = defaultdict(list)

    def remove_subscriptions(self):
        self._callbacks = defaultdict(list)

    def subscribe(self, signal_name: str, func: Callable[..., Any]):
        self._callbacks[signal_name].append(func)

    def notify(self, signal_name: str, *args: Any, **kwargs: dict[str, Any]):
        if self._frozen:
            self._frozen = False
            return

        self._log.info('Signal: %s', signal_name)
        callbacks = self._callbacks.get(signal_name, [])
        for func in callbacks:
            func(self, signal_name, *args, **kwargs) class LogAdapter(LoggerAdapter):

    def set_context(self, context):
        self.extra['context'] = context

    def process(self, msg, kwargs):
        return '(%s) %s' % (self.extra['context'], msg), kwargs
import logging
import re
import time
from xml.parsers.expat import ExpatError
from gi.repository import GLib
from nbxmpp.simplexml import NodeBuilder
from nbxmpp.simplexml import Node
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.protocol import InvalidFrom
from nbxmpp.protocol import InvalidJid
from nbxmpp.protocol import InvalidStanza
from nbxmpp.protocol import Iq
from nbxmpp.protocol import Presence
from nbxmpp.protocol import Message
from nbxmpp.protocol import Protocol
from nbxmpp.protocol import Error
from nbxmpp.protocol import StreamErrorNode
from nbxmpp.protocol import ERR_FEATURE_NOT_IMPLEMENTED
from nbxmpp.modules.eme import EME
from nbxmpp.modules.http_auth import HTTPAuth
from nbxmpp.modules.presence import BasePresence
from nbxmpp.modules.message import BaseMessage
from nbxmpp.modules.iq import BaseIq
from nbxmpp.modules.nickname import Nickname
from nbxmpp.modules.delay import Delay
from nbxmpp.modules.muc import MUC
from nbxmpp.modules.muc.moderation import Moderation
from nbxmpp.modules.idle import Idle
from nbxmpp.modules.pgplegacy import PGPLegacy
from nbxmpp.modules.vcard_avatar import VCardAvatar
from nbxmpp.modules.captcha import Captcha
from nbxmpp.modules.entity_caps import EntityCaps
from nbxmpp.modules.blocking import Blocking
from nbxmpp.modules.pubsub import PubSub
from nbxmpp.modules.activity import Activity
from nbxmpp.modules.tune import Tune
from nbxmpp.modules.mood import Mood
from nbxmpp.modules.location import Location
from nbxmpp.modules.user_avatar import UserAvatar
from nbxmpp.modules.bookmarks.private_bookmarks import PrivateBookmarks
from nbxmpp.modules.bookmarks.pep_bookmarks import PEPBookmarks
from nbxmpp.modules.bookmarks.native_bookmarks import NativeBookmarks
from nbxmpp.modules.openpgp import OpenPGP
from nbxmpp.modules.omemo import OMEMO
from nbxmpp.modules.annotations import Annotations
from nbxmpp.modules.muclumbus import Muclumbus
from nbxmpp.modules.software_version import SoftwareVersion
from nbxmpp.modules.adhoc import AdHoc
from nbxmpp.modules.ibb import IBB
from nbxmpp.modules.discovery import Discovery
from nbxmpp.modules.chat_markers import ChatMarkers
from nbxmpp.modules.receipts import Receipts
from nbxmpp.modules.oob import OOB
from nbxmpp.modules.correction import Correction
from nbxmpp.modules.attention import Attention
from nbxmpp.modules.security_labels import SecurityLabels
from nbxmpp.modules.chatstates import Chatstates
from nbxmpp.modules.register import Register
from nbxmpp.modules.http_upload import HTTPUpload
from nbxmpp.modules.mam import MAM
from nbxmpp.modules.vcard_temp import VCardTemp
from nbxmpp.modules.vcard4 import VCard4
from nbxmpp.modules.ping import Ping
from nbxmpp.modules.delimiter import Delimiter
from nbxmpp.modules.roster import Roster
from nbxmpp.modules.last_activity import LastActivity
from nbxmpp.modules.entity_time import EntityTime
from nbxmpp.modules.misc import unwrap_carbon
from nbxmpp.modules.misc import unwrap_mam
from nbxmpp.util import get_properties_struct
from nbxmpp.util import get_invalid_xml_regex
from nbxmpp.util import is_websocket_close
from nbxmpp.util import is_websocket_stream_error
from nbxmpp.util import Observable
from nbxmpp.util import LogAdapter
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see <http://www.gnu.org/licenses/>.





log = logging.getLogger('nbxmpp.dispatcher')


class StanzaDispatcher(Observable):
    """
    Dispatches stanzas to handlers

    Signals:
        before-dispatch
        parsing-error
        stream-end

    """

    def __init__(self, client):
        Observable.__init__(self, log)
        self._client = client
        self._modules = {}
        self._parser = None
        self._websocket_stream_error = None

        self._log = LogAdapter(log, {'context': client.log_context})

        self._handlers = {}

        self._id_callbacks = {}
        self._dispatch_callback = None
        self._timeout_id = None

        self._stanza_types = {
            'iq': Iq,
            'message': Message,
            'presence': Presence,
            'error': StreamErrorNode,
        }

        self.invalid_chars_re = get_invalid_xml_regex()

        self._register_namespace('unknown')
        self._register_namespace(Namespace.STREAMS)
        self._register_namespace(Namespace.CLIENT)
        self._register_protocol('iq', Iq)
        self._register_protocol('presence', Presence)

        self._register_protocol('message', Message)