This commit is contained in:
2026-08-02 18:57:40 +02:00
parent 5e5ab8681a
commit 6fa59321c0
5759 changed files with 712133 additions and 66 deletions
@@ -0,0 +1,63 @@
########################################################################
# File name: __init__.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
"""
:mod:`~aioxmpp.entitycaps` --- Entity Capabilities support (:xep:`390`, :xep:`0115`)
####################################################################################
This module provides support for :xep:`XEP-0115 (Entity Capabilities) <0115>`
and :xep:`XEP-0390 (Entity Capabilities 2.0) <0390>`. To use it,
:meth:`.Client.summon` the :class:`aioxmpp.EntityCapsService` on a
:class:`~.Client`. See the service documentation for more information.
.. versionadded:: 0.5
.. versionchanged:: 0.9
Support for :xep:`390` was added.
Service
=======
.. currentmodule:: aioxmpp
.. autoclass:: EntityCapsService
.. currentmodule:: aioxmpp.entitycaps
.. class:: Service
Alias of :class:`.EntityCapsService`.
.. deprecated:: 0.8
The alias will be removed in 1.0.
.. autoclass:: Cache
.. currentmodule:: aioxmpp.entitycaps.xso
""" # NOQA: E501
from .service import EntityCapsService, Cache # NOQA: F401
from . import xso # NOQA: F401
Service = EntityCapsService
@@ -0,0 +1,175 @@
########################################################################
# File name: caps115.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
import base64
import collections
import hashlib
import pathlib
import urllib.parse
from xml.sax.saxutils import escape
from .common import AbstractKey, AbstractImplementation
from . import xso as caps_xso
def build_identities_string(identities):
identities = [
b"/".join([
escape(identity.category).encode("utf-8"),
escape(identity.type_).encode("utf-8"),
escape(str(identity.lang or "")).encode("utf-8"),
escape(identity.name or "").encode("utf-8"),
])
for identity in identities
]
if len(set(identities)) != len(identities):
raise ValueError("duplicate identity")
identities.sort()
identities.append(b"")
return b"<".join(identities)
def build_features_string(features):
features = list(escape(feature).encode("utf-8") for feature in features)
if len(set(features)) != len(features):
raise ValueError("duplicate feature")
features.sort()
features.append(b"")
return b"<".join(features)
def build_forms_string(forms):
types = set()
forms_list = []
for form in forms:
try:
form_types = set(
value
for field in form.fields.filter(attrs={"var": "FORM_TYPE"})
for value in field.values
)
except KeyError:
continue
if len(form_types) > 1:
raise ValueError("form with multiple types")
elif not form_types:
continue
type_ = escape(next(iter(form_types))).encode("utf-8")
if type_ in types:
raise ValueError("multiple forms of type {!r}".format(type_))
types.add(type_)
forms_list.append((type_, form))
forms_list.sort()
parts = []
for type_, form in forms_list:
parts.append(type_)
field_list = sorted(
(
(escape(field.var).encode("utf-8"), field.values)
for field in form.fields
if field.var != "FORM_TYPE"
),
key=lambda x: x[0]
)
for var, values in field_list:
parts.append(var)
parts.extend(sorted(
escape(value).encode("utf-8") for value in values
))
parts.append(b"")
return b"<".join(parts)
def hash_query(query, algo):
hashimpl = hashlib.new(algo)
hashimpl.update(
build_identities_string(query.identities)
)
hashimpl.update(
build_features_string(query.features)
)
hashimpl.update(
build_forms_string(query.exts)
)
return base64.b64encode(hashimpl.digest()).decode("ascii")
Key = collections.namedtuple("Key", ["algo", "node"])
class Key(Key, AbstractKey):
@property
def path(self):
quoted = urllib.parse.quote(self.node, safe="")
return (pathlib.Path("hashes") /
"{}_{}.xml".format(self.algo, quoted))
@property
def ver(self):
return self.node.rsplit("#", 1)[1]
def verify(self, query_response):
digest_b64 = hash_query(query_response, self.algo.replace("-", ""))
return self.ver == digest_b64
class Implementation(AbstractImplementation):
def __init__(self, node, **kwargs):
super().__init__(**kwargs)
self.__node = node
def extract_keys(self, obj):
caps = obj.xep0115_caps
if caps is None or caps.hash_ is None:
return
yield Key(caps.hash_, "{}#{}".format(caps.node, caps.ver))
def put_keys(self, keys, presence):
key, = keys
presence.xep0115_caps = caps_xso.Caps115(
self.__node,
key.ver,
key.algo,
)
def calculate_keys(self, query_response):
yield Key(
"sha-1",
"{}#{}".format(
self.__node,
hash_query(query_response, "sha1"),
)
)
@@ -0,0 +1,192 @@
########################################################################
# File name: caps390.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
import base64
import pathlib
import collections
import urllib.parse
import aioxmpp.hashes
from .common import AbstractKey
from . import xso as caps_xso
def _process_features(features):
"""
Generate the `Features String` from an iterable of features.
:param features: The features to generate the features string from.
:type features: :class:`~collections.abc.Iterable` of :class:`str`
:return: The `Features String`
:rtype: :class:`bytes`
Generate the `Features String` from the given `features` as specified in
:xep:`390`.
"""
parts = [
feature.encode("utf-8")+b"\x1f"
for feature in features
]
parts.sort()
return b"".join(parts)+b"\x1c"
def _process_identity(identity):
category = (identity.category or "").encode("utf-8")+b"\x1f"
type_ = (identity.type_ or "").encode("utf-8")+b"\x1f"
lang = str(identity.lang or "").encode("utf-8")+b"\x1f"
name = (identity.name or "").encode("utf-8")+b"\x1f"
return b"".join([category, type_, lang, name]) + b"\x1e"
def _process_identities(identities):
"""
Generate the `Identities String` from an iterable of identities.
:param identities: The identities to generate the features string from.
:type identities: :class:`~collections.abc.Iterable` of
:class:`~.disco.xso.Identity`
:return: The `Identities String`
:rtype: :class:`bytes`
Generate the `Identities String` from the given `identities` as specified
in :xep:`390`.
"""
parts = [
_process_identity(identity)
for identity in identities
]
parts.sort()
return b"".join(parts)+b"\x1c"
def _process_field(field):
parts = [
(value or "").encode("utf-8") + b"\x1f"
for value in field.values
]
parts.insert(0, field.var.encode("utf-8")+b"\x1f")
return b"".join(parts)+b"\x1e"
def _process_form(form):
parts = [
_process_field(form)
for form in form.fields
]
parts.sort()
return b"".join(parts)+b"\x1d"
def _process_extensions(exts):
"""
Generate the `Extensions String` from an iterable of data forms.
:param exts: The data forms to generate the extensions string from.
:type exts: :class:`~collections.abc.Iterable` of
:class:`~.forms.xso.Data`
:return: The `Extensions String`
:rtype: :class:`bytes`
Generate the `Extensions String` from the given `exts` as specified
in :xep:`390`.
"""
parts = [
_process_form(form)
for form in exts
]
parts.sort()
return b"".join(parts)+b"\x1c"
def _get_hash_input(info):
return b"".join([
_process_features(info.features),
_process_identities(info.identities),
_process_extensions(info.exts)
])
def _calculate_hash(algo, hash_input):
impl = aioxmpp.hashes.hash_from_algo(algo)
impl.update(hash_input)
return impl.digest()
Key = collections.namedtuple("Key", ["algo", "digest"])
class Key(Key, AbstractKey):
@property
def node(self):
return "urn:xmpp:caps#{}.{}".format(
self.algo,
base64.b64encode(self.digest).decode("ascii")
)
@property
def path(self):
encoded = base64.b32encode(
self.digest
).decode("ascii").rstrip("=").lower()
return (pathlib.Path("caps2") /
urllib.parse.quote(self.algo, safe="") /
encoded[:2] /
encoded[2:4] /
"{}.xml".format(encoded[4:]))
def verify(self, info):
if not isinstance(info, bytes):
info = _get_hash_input(info)
digest = _calculate_hash(self.algo, info)
return digest == self.digest
class Implementation:
def __init__(self, algorithms, **kwargs):
super().__init__(**kwargs)
self.__algorithms = algorithms
def extract_keys(self, presence):
if presence.xep0390_caps is None:
return ()
return (
Key(algo, digest)
for algo, digest in presence.xep0390_caps.digests.items()
if aioxmpp.hashes.is_algo_supported(algo)
)
def put_keys(self, keys, presence):
presence.xep0390_caps = caps_xso.Caps390()
presence.xep0390_caps.digests.update({
key.algo: key.digest
for key in keys
})
def calculate_keys(self, query_response):
input = _get_hash_input(query_response)
for algo in self.__algorithms:
yield Key(algo, _calculate_hash(algo, input))
@@ -0,0 +1,105 @@
########################################################################
# File name: common.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
import abc
class AbstractKey(metaclass=abc.ABCMeta):
@abc.abstractproperty
def path(self):
"""
Return the file system path relative to the root of a file-system based
caps database for this key.
The path includes all information of the key. Components of the path do
not exceed 255 codepoints and use only ASCII codepoints.
If it is not possible to create such a path, :class:`ValueError` is
raised.
"""
@abc.abstractmethod
def verify(self, query_response):
"""
Verify whether the cache key matches a piece of service discovery
information.
:param query_response: The full :xep:`30` disco#info query response.
:type query_response: :class:`~.disco.xso.InfoQuery`
:rtype: :class:`bool`
:return: true if the key matches and false otherwise.
"""
class AbstractImplementation(metaclass=abc.ABCMeta):
@abc.abstractmethod
def extract_keys(self, presence):
"""
Extract cache keys from a presence stanza.
:param presence: Presence stanza to extract cache keys from.
:type presence: :class:`aioxmpp.Presence`
:rtype: :class:`~collections.abc.Iterable` of :class:`AbstractKey`
:return: The cache keys from the presence stanza.
The resulting iterable may be empty if the presence stanza does not
carry any capabilities information with it.
The resulting iterable cannot be iterated over multiple times.
"""
@abc.abstractmethod
def put_keys(self, keys, presence):
"""
Insert cache keys into a presence stanza.
:param keys: An iterable of cache keys to insert.
:type keys: :class:`~collections.abc.Iterable` of :class:`AbstractKey`
objects
:param presence: The presence stanza into which the cache keys shall be
injected.
:type presence: :class:`aioxmpp.Presence`
The presence stanza is modified in-place.
"""
@abc.abstractmethod
def calculate_keys(self, query_response):
"""
Calculate the cache keys for a disco#info response.
:param query_response: The full :xep:`30` disco#info query response.
:type query_response: :class:`~.disco.xso.InfoQuery`
:rtype: :class:`~collections.abc.Iterable` of :class:`AbstractKey`
:return: An iterable of the cache keys for the disco#info response.
..
:param identities: The identities of the disco#info response.
:type identities: :class:`~collections.abc.Iterable` of
:class:`~.disco.xso.Identity`
:param features: The features of the disco#info response.
:type features: :class:`~collections.abc.Iterable` of
:class:`str`
:param features: The extensions of the disco#info response.
:type features: :class:`~collections.abc.Iterable` of
:class:`~.forms.xso.Data`
"""
@@ -0,0 +1,513 @@
########################################################################
# File name: service.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
import asyncio
import collections
import copy
import functools
import logging
import os
import tempfile
import aioxmpp.callbacks
import aioxmpp.disco as disco
import aioxmpp.service
import aioxmpp.utils
import aioxmpp.xml
import aioxmpp.xso
from aioxmpp.utils import namespaces
from . import caps115, caps390
logger = logging.getLogger("aioxmpp.entitycaps")
class Cache:
"""
This provides a two-level cache for entity capabilities information. The
idea is to have a trusted database, e.g. installed system-wide or shipped
with :mod:`aioxmpp` and in addition a user-level database which is
automatically filled with hashes which have been found by the
:class:`Service`.
The trusted database is taken as read-only and overrides the user-collected
database. When a hash is in both databases, it is removed from the
user-collected database (to save space).
In addition to serving the databases, it provides deduplication for queries
by holding a cache of futures looking up the same hash.
Database management (user API):
.. automethod:: set_system_db_path
.. automethod:: set_user_db_path
Queries (API intended for :class:`Service`):
.. automethod:: create_query_future
.. automethod:: lookup_in_database
.. automethod:: lookup
"""
def __init__(self):
self._lookup_cache = {}
self._memory_overlay = {}
self._system_db_path = None
self._user_db_path = None
def _erase_future(self, key, fut):
try:
existing = self._lookup_cache[key]
except KeyError:
pass
else:
if existing is fut:
del self._lookup_cache[key]
def set_system_db_path(self, path):
self._system_db_path = path
def set_user_db_path(self, path):
self._user_db_path = path
def lookup_in_database(self, key):
try:
result = self._memory_overlay[key]
except KeyError:
pass
else:
logger.debug("memory cache hit: %s", key)
return result
key_path = key.path
if self._system_db_path is not None:
try:
f = (
self._system_db_path / key_path
).open("rb")
except OSError:
pass
else:
logger.debug("system db hit: %s", key)
with f:
return aioxmpp.xml.read_single_xso(f, disco.xso.InfoQuery)
if self._user_db_path is not None:
try:
f = (
self._user_db_path / key_path
).open("rb")
except OSError:
pass
else:
logger.debug("user db hit: %s", key)
with f:
return aioxmpp.xml.read_single_xso(f, disco.xso.InfoQuery)
raise KeyError(key)
async def lookup(self, key):
"""
Look up the given `node` URL using the given `hash_` first in the
database and then by waiting on the futures created with
:meth:`create_query_future` for that node URL and hash.
If the hash is not in the database, :meth:`lookup` iterates as long as
there are pending futures for the given `hash_` and `node`. If there
are no pending futures, :class:`KeyError` is raised. If a future raises
a :class:`ValueError`, it is ignored. If the future returns a value, it
is used as the result.
"""
try:
result = self.lookup_in_database(key)
except KeyError:
pass
else:
return result
while True:
fut = self._lookup_cache[key]
try:
result = await fut
except ValueError:
continue
else:
return result
def create_query_future(self, key):
"""
Create and return a :class:`asyncio.Future` for the given `hash_`
function and `node` URL. The future is referenced internally and used
by any calls to :meth:`lookup` which are made while the future is
pending. The future is removed from the internal storage automatically
when a result or exception is set for it.
This allows for deduplication of queries for the same hash.
"""
fut = asyncio.Future()
fut.add_done_callback(
functools.partial(self._erase_future, key)
)
self._lookup_cache[key] = fut
return fut
def add_cache_entry(self, key, entry):
"""
Add the given `entry` (which must be a :class:`~.disco.xso.InfoQuery`
instance) to the user-level database keyed with the hash function type
`hash_` and the `node` URL. The `entry` is **not** validated to
actually map to `node` with the given `hash_` function, it is expected
that the caller performs the validation.
"""
copied_entry = copy.copy(entry)
self._memory_overlay[key] = copied_entry
if self._user_db_path is not None:
asyncio.ensure_future(asyncio.get_event_loop().run_in_executor(
None,
writeback,
self._user_db_path / key.path,
entry.captured_events))
class EntityCapsService(aioxmpp.service.Service):
"""
Make use and provide service discovery information in presence broadcasts.
This service implements :xep:`0115` and :xep:`0390`, transparently.
Besides loading the service, no interaction is required to get some of
the benefits of :xep:`0115` and :xep:`0390`.
Two additional things need to be done by users to get full support and
performance:
1. To make sure that peers are always up-to-date with the current
capabilities, it is required that users listen on the
:meth:`on_ver_changed` signal and re-emit their current presence when it
fires.
.. note::
Keeping peers up-to-date is a MUST in :xep:`390`.
The service takes care of attaching capabilities information on the
outgoing stanza, using a stanza filter.
.. warning::
:meth:`on_ver_changed` may be emitted at a considerable rate when
services are loaded or certain features (such as PEP-based services)
are configured. It is up to the application to limit the rate at
which presences are sent for the sole purpose of updating peers with
new capability information.
2. Users should use a process-wide :class:`Cache` instance and assign it to
the :attr:`cache` of each :class:`.entitycaps.Service` they use. This
improves performance by sharing (verified) hashes among :class:`Service`
instances.
In addition, the hashes should be saved and restored on shutdown/start
of the process. See the :class:`Cache` for details.
.. signal:: on_ver_changed
The signal emits whenever the Capability Hashset of the local client
changes. This happens when the set of features or identities announced
in the :class:`.DiscoServer` changes.
.. autoattribute:: cache
.. autoattribute:: xep115_support
.. autoattribute:: xep390_support
.. versionchanged:: 0.8
This class was formerly known as :class:`aioxmpp.entitycaps.Service`. It
is still available under that name, but the alias will be removed in
1.0.
.. versionchanged:: 0.9
Support for :xep:`390` was added.
"""
ORDER_AFTER = {
disco.DiscoClient,
disco.DiscoServer,
}
NODE = "http://aioxmpp.zombofant.net/"
on_ver_changed = aioxmpp.callbacks.Signal()
def __init__(self, node, **kwargs):
super().__init__(node, **kwargs)
self.__current_keys = {}
self._cache = Cache()
self.disco_server = self.dependencies[disco.DiscoServer]
self.disco_client = self.dependencies[disco.DiscoClient]
self.__115 = caps115.Implementation(self.NODE)
self.__390 = caps390.Implementation(
aioxmpp.hashes.default_hash_algorithms
)
self.__active_hashsets = []
self.__key_users = collections.Counter()
@property
def xep115_support(self):
"""
Boolean to control whether :xep:`115` support is enabled or not.
Defaults to :data:`True`.
If set to false, inbound :xep:`115` capabilities will not be processed
and no :xep:`115` capabilities will be emitted.
.. note::
At some point, this will default to :data:`False` to save
bandwidth. The exact release depends on the adoption of :xep:`390`
and will be announced in time. If you depend on :xep:`115` support,
set this boolean to :data:`True`.
The attribute itself will not be removed until :xep:`115` support
is removed from :mod:`aioxmpp` entirely, which is unlikely to
happen any time soon.
.. versionadded:: 0.9
"""
return self._xep115_feature.enabled
@xep115_support.setter
def xep115_support(self, value):
self._xep115_feature.enabled = value
@property
def xep390_support(self):
"""
Boolean to control whether :xep:`390` support is enabled or not.
Defaults to :data:`True`.
If set to false, inbound :xep:`390` Capability Hash Sets will not be
processed and no Capability Hash Sets or Capability Nodes will be
generated.
The hash algorithms used for generating Capability Hash Sets are those
from :data:`aioxmpp.hashes.default_hash_algorithms`.
"""
return self._xep390_feature.enabled
@xep390_support.setter
def xep390_support(self, value):
self._xep390_feature.enabled = value
@property
def cache(self):
"""
The :class:`Cache` instance used for this :class:`Service`. Deleting
this attribute will automatically create a new :class:`Cache` instance.
The attribute can be used to share a single :class:`Cache` among
multiple :class:`Service` instances.
"""
return self._cache
@cache.setter
def cache(self, v):
self._cache = v
@cache.deleter
def cache(self):
self._cache = Cache()
@aioxmpp.service.depsignal(
disco.DiscoServer,
"on_info_changed")
def _info_changed(self):
self.logger.debug("info changed, scheduling re-calculation of version")
asyncio.get_event_loop().call_soon(
self.update_hash
)
async def _shutdown(self):
for group in self.__current_keys.values():
for key in group:
self.disco_server.unmount_node(key.node)
async def query_and_cache(self, jid, key, fut):
data = await self.disco_client.query_info(
jid,
node=key.node,
require_fresh=True,
no_cache=True, # the caps node is never queried by apps
)
try:
if key.verify(data):
self.cache.add_cache_entry(key, data)
fut.set_result(data)
else:
raise ValueError("hash mismatch")
except ValueError as exc:
fut.set_exception(exc)
return data
async def lookup_info(self, jid, keys):
for key in keys:
try:
info = await self.cache.lookup(key)
except KeyError:
continue
self.logger.debug("found %s in cache", key)
return info
first_key = keys[0]
self.logger.debug("using key %s to query peer", first_key)
fut = self.cache.create_query_future(first_key)
info = await self.query_and_cache(
jid, first_key, fut
)
self.logger.debug("%s maps to %r", key, info)
return info
@aioxmpp.service.outbound_presence_filter
def handle_outbound_presence(self, presence):
if (presence.type_ == aioxmpp.structs.PresenceType.AVAILABLE
and self.__active_hashsets):
current_hashset = self.__active_hashsets[-1]
try:
keys = current_hashset[self.__115]
except KeyError:
pass
else:
self.__115.put_keys(keys, presence)
try:
keys = current_hashset[self.__390]
except KeyError:
pass
else:
self.__390.put_keys(keys, presence)
return presence
@aioxmpp.service.inbound_presence_filter
def handle_inbound_presence(self, presence):
keys = []
if self.xep390_support:
keys.extend(self.__390.extract_keys(presence))
if self.xep115_support:
keys.extend(self.__115.extract_keys(presence))
if keys:
lookup_task = aioxmpp.utils.LazyTask(
self.lookup_info,
presence.from_,
keys,
)
self.disco_client.set_info_future(
presence.from_,
None,
lookup_task
)
return presence
def _push_hashset(self, node, hashset):
if self.__active_hashsets and hashset == self.__active_hashsets[-1]:
return False
for group in hashset.values():
for key in group:
if not self.__key_users[key.node]:
self.disco_server.mount_node(key.node, node)
self.__key_users[key.node] += 1
self.__active_hashsets.append(hashset)
for expired in self.__active_hashsets[:-3]:
for group in expired.values():
for key in group:
self.__key_users[key.node] -= 1
if not self.__key_users[key.node]:
self.disco_server.unmount_node(key.node)
del self.__key_users[key.node]
del self.__active_hashsets[:-3]
return True
def update_hash(self):
node = disco.StaticNode.clone(self.disco_server)
info = node.as_info_xso()
new_hashset = {}
if self.xep115_support:
new_hashset[self.__115] = set(self.__115.calculate_keys(info))
if self.xep390_support:
new_hashset[self.__390] = set(self.__390.calculate_keys(info))
self.logger.debug("new hashset=%r", new_hashset)
if self._push_hashset(node, new_hashset):
self.on_ver_changed()
# declare those at the bottom so that on_ver_changed gets emitted when the
# service is instantiated
_xep115_feature = disco.register_feature(namespaces.xep0115_caps)
_xep390_feature = disco.register_feature(namespaces.xep0390_caps)
def writeback(path, captured_events):
aioxmpp.utils.mkdir_exist_ok(path.parent)
with tempfile.NamedTemporaryFile(dir=str(path.parent),
delete=False) as tmpf:
try:
generator = aioxmpp.xml.XMPPXMLGenerator(
tmpf,
short_empty_elements=True)
generator.startDocument()
aioxmpp.xso.events_to_sax(captured_events, generator)
generator.endDocument()
except: # NOQA
os.unlink(tmpf.name)
raise
os.replace(tmpf.name, str(path))
@@ -0,0 +1,82 @@
########################################################################
# File name: xso.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
import aioxmpp.hashes
import aioxmpp.stanza as stanza
import aioxmpp.xso as xso
from aioxmpp.utils import namespaces
namespaces.xep0115_caps = "http://jabber.org/protocol/caps"
namespaces.xep0390_caps = "urn:xmpp:caps"
class Caps115(xso.XSO):
"""
An entity capabilities extension for :class:`~.Presence`.
.. attribute:: node
The indicated node, for use with the corresponding info query.
.. attribute:: hash_
The hash algorithm used. This is :data:`None` if the legacy format is
used.
.. attribute:: ver
The version (in the legacy format) or the calculated hash.
.. attribute:: ext
Only there for backwards compatibility. Not used anymore.
"""
TAG = (namespaces.xep0115_caps, "c")
node = xso.Attr("node")
hash_ = xso.Attr(
"hash",
validator=xso.Nmtoken(),
validate=xso.ValidateMode.FROM_CODE,
default=None # to check for legacy
)
ver = xso.Attr("ver")
ext = xso.Attr("ext", default=None)
def __init__(self, node, ver, hash_):
super().__init__()
self.node = node
self.ver = ver
self.hash_ = hash_
class Caps390(aioxmpp.hashes.HashesParent, xso.XSO):
TAG = namespaces.xep0390_caps, "c"
stanza.Presence.xep0115_caps = xso.Child([Caps115])
stanza.Presence.xep0390_caps = xso.Child([Caps390])