v1.3.5
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
########################################################################
|
||||
# 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.bookmarks` – Bookmark support (:xep:`0048`)
|
||||
##########################################################
|
||||
|
||||
This module provides support for storing and retrieving bookmarks on
|
||||
the server as per :xep:`Bookmarks <48>`.
|
||||
|
||||
Service
|
||||
=======
|
||||
|
||||
.. currentmodule:: aioxmpp
|
||||
|
||||
.. autoclass:: BookmarkClient
|
||||
|
||||
.. currentmodule:: aioxmpp.bookmarks
|
||||
|
||||
XSOs
|
||||
====
|
||||
|
||||
All bookmark types must adhere to the following ABC:
|
||||
|
||||
.. autoclass:: Bookmark
|
||||
|
||||
The following XSOs are used to represent an manipulate bookmark lists.
|
||||
|
||||
.. autoclass:: Conference
|
||||
|
||||
.. autoclass:: URL
|
||||
|
||||
To register custom bookmark classes use:
|
||||
|
||||
.. autofunction:: as_bookmark_class
|
||||
|
||||
The following is used internally as the XSO container for bookmarks.
|
||||
|
||||
.. autoclass:: Storage
|
||||
|
||||
Notes on usage
|
||||
==============
|
||||
|
||||
.. currentmodule:: aioxmpp
|
||||
|
||||
It is highly recommended to interact with the bookmark client via the
|
||||
provided signals and the get-modify-set methods
|
||||
:meth:`~BookmarkClient.add_bookmark`,
|
||||
:meth:`~BookmarkClient.discard_bookmark` and
|
||||
:meth:`~BookmarkClient.update_bookmark`. Using
|
||||
:meth:`~BookmarkClient.set_bookmarks` directly is error prone and
|
||||
might cause data loss due to race conditions.
|
||||
|
||||
"""
|
||||
|
||||
from .xso import (Storage, Bookmark, Conference, URL, # NOQA: F401
|
||||
as_bookmark_class)
|
||||
from .service import BookmarkClient # NOQA: F401
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,470 @@
|
||||
########################################################################
|
||||
# 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 aioxmpp
|
||||
import aioxmpp.callbacks as callbacks
|
||||
import aioxmpp.service as service
|
||||
import aioxmpp.private_xml as private_xml
|
||||
|
||||
from . import xso as bookmark_xso
|
||||
|
||||
|
||||
# TODO: use private storage in pubsub where available.
|
||||
# TODO: sync bookmarks between pubsub and private xml storage
|
||||
# TODO: do we need merge-capabilities to reconcile the bookmarks
|
||||
# from different sources (local bookmark storage, pubsub, private xml
|
||||
# storage)
|
||||
class BookmarkClient(service.Service):
|
||||
"""
|
||||
Supports retrieval and storage of bookmarks on the server.
|
||||
It currently only supports :xep:`Private XML Storage <49>` as
|
||||
backend.
|
||||
|
||||
There is the general rule *never* to modify the bookmark instances
|
||||
retrieved from this class (either by :meth:`get_bookmarks` or as
|
||||
an argument to one of the signals). If you need to modify a bookmark
|
||||
for use with :meth:`update_bookmark` use :func:`copy.copy` to create
|
||||
a copy.
|
||||
|
||||
.. automethod:: sync
|
||||
|
||||
.. automethod:: get_bookmarks
|
||||
|
||||
.. automethod:: set_bookmarks
|
||||
|
||||
The following methods change the bookmark list in a get-modify-set
|
||||
pattern, to mitigate the danger of race conditions and should be
|
||||
used in most circumstances:
|
||||
|
||||
.. automethod:: add_bookmark
|
||||
|
||||
.. automethod:: discard_bookmark
|
||||
|
||||
.. automethod:: update_bookmark
|
||||
|
||||
|
||||
The following signals are provided that allow tracking the changes to
|
||||
the bookmark list:
|
||||
|
||||
.. signal:: on_bookmark_added(added_bookmark)
|
||||
|
||||
Fires when a new bookmark is added.
|
||||
|
||||
.. signal:: on_bookmark_removed(removed_bookmark)
|
||||
|
||||
Fires when a bookmark is removed.
|
||||
|
||||
.. signal:: on_bookmark_changed(old_bookmark, new_bookmark)
|
||||
|
||||
Fires when a bookmark is changed.
|
||||
|
||||
.. note:: A heuristic is used to determine the change of bookmarks
|
||||
and the reported changes may not directly reflect the
|
||||
used methods, but it will always be possible to
|
||||
construct the list of bookmarks from the events. For
|
||||
example, when using :meth:`update_bookmark` to change
|
||||
the JID of a :class:`Conference` bookmark a removed and
|
||||
a added signal will fire.
|
||||
|
||||
.. note:: The bookmark protocol is prone to race conditions if
|
||||
several clients access it concurrently. Be careful to
|
||||
use a get-modify-set pattern or the provided highlevel
|
||||
interface.
|
||||
|
||||
.. note:: Some other clients extend the bookmark format. For now
|
||||
those extensions are silently dropped by our XSOs, and
|
||||
therefore are lost, when changing the bookmarks with
|
||||
aioxmpp. This is considered a bug to be fixed in the future.
|
||||
"""
|
||||
|
||||
ORDER_AFTER = [
|
||||
private_xml.PrivateXMLService,
|
||||
]
|
||||
|
||||
on_bookmark_added = callbacks.Signal()
|
||||
on_bookmark_removed = callbacks.Signal()
|
||||
on_bookmark_changed = callbacks.Signal()
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
super().__init__(client, **kwargs)
|
||||
self._private_xml = self.dependencies[private_xml.PrivateXMLService]
|
||||
self._bookmark_cache = []
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
@service.depsignal(aioxmpp.Client, "on_stream_established", defer=True)
|
||||
async def _stream_established(self):
|
||||
await self.sync()
|
||||
|
||||
async def _get_bookmarks(self):
|
||||
"""
|
||||
Get the stored bookmarks from the server.
|
||||
|
||||
:returns: a list of bookmarks
|
||||
"""
|
||||
res = await self._private_xml.get_private_xml(
|
||||
bookmark_xso.Storage()
|
||||
)
|
||||
|
||||
return res.registered_payload.bookmarks
|
||||
|
||||
async def _set_bookmarks(self, bookmarks):
|
||||
"""
|
||||
Set the bookmarks stored on the server.
|
||||
"""
|
||||
storage = bookmark_xso.Storage()
|
||||
storage.bookmarks[:] = bookmarks
|
||||
await self._private_xml.set_private_xml(storage)
|
||||
|
||||
def _diff_emit_update(self, new_bookmarks):
|
||||
"""
|
||||
Diff the bookmark cache and the new bookmark state, emit signals as
|
||||
needed and set the bookmark cache to the new data.
|
||||
"""
|
||||
|
||||
self.logger.debug("diffing %s, %s", self._bookmark_cache,
|
||||
new_bookmarks)
|
||||
|
||||
def subdivide(level, old, new):
|
||||
"""
|
||||
Subdivide the bookmarks according to the data item
|
||||
``bookmark.secondary[level]`` and emit the appropriate
|
||||
events.
|
||||
"""
|
||||
if len(old) == len(new) == 1:
|
||||
old_entry = old.pop()
|
||||
new_entry = new.pop()
|
||||
if old_entry == new_entry:
|
||||
pass
|
||||
else:
|
||||
self.on_bookmark_changed(old_entry, new_entry)
|
||||
return ([], [])
|
||||
|
||||
elif len(old) == 0:
|
||||
return ([], new)
|
||||
|
||||
elif len(new) == 0:
|
||||
return (old, [])
|
||||
|
||||
else:
|
||||
try:
|
||||
groups = {}
|
||||
for entry in old:
|
||||
group = groups.setdefault(
|
||||
entry.secondary[level],
|
||||
([], [])
|
||||
)
|
||||
group[0].append(entry)
|
||||
|
||||
for entry in new:
|
||||
group = groups.setdefault(
|
||||
entry.secondary[level],
|
||||
([], [])
|
||||
)
|
||||
group[1].append(entry)
|
||||
except IndexError:
|
||||
# the classification is exhausted, this means
|
||||
# all entries in this bin are equal by the
|
||||
# definition of bookmark equivalence!
|
||||
common = min(len(old), len(new))
|
||||
assert old[:common] == new[:common]
|
||||
return (old[common:], new[common:])
|
||||
|
||||
old_unhandled, new_unhandled = [], []
|
||||
for old, new in groups.values():
|
||||
unhandled = subdivide(level+1, old, new)
|
||||
old_unhandled += unhandled[0]
|
||||
new_unhandled += unhandled[1]
|
||||
|
||||
# match up unhandleds as changes as early as possible
|
||||
i = -1
|
||||
for i, (old_entry, new_entry) in enumerate(
|
||||
zip(old_unhandled, new_unhandled)):
|
||||
self.logger.debug("changed %s -> %s", old_entry, new_entry)
|
||||
self.on_bookmark_changed(old_entry, new_entry)
|
||||
i += 1
|
||||
return old_unhandled[i:], new_unhandled[i:]
|
||||
|
||||
# group the bookmarks into groups whose elements may transform
|
||||
# among one another by on_bookmark_changed events. This information
|
||||
# is given by the type of the bookmark and the .primary property
|
||||
changable_groups = {}
|
||||
|
||||
for item in self._bookmark_cache:
|
||||
group = changable_groups.setdefault(
|
||||
(type(item), item.primary),
|
||||
([], [])
|
||||
)
|
||||
group[0].append(item)
|
||||
|
||||
for item in new_bookmarks:
|
||||
group = changable_groups.setdefault(
|
||||
(type(item), item.primary),
|
||||
([], [])
|
||||
)
|
||||
group[1].append(item)
|
||||
|
||||
for old, new in changable_groups.values():
|
||||
|
||||
# the first branches are fast paths which should catch
|
||||
# most cases – especially all cases where each bare jid of
|
||||
# a conference bookmark or each url of an url bookmark is
|
||||
# only used in one bookmark
|
||||
if len(old) == len(new) == 1:
|
||||
old_entry = old.pop()
|
||||
new_entry = new.pop()
|
||||
if old_entry == new_entry:
|
||||
# the bookmark is unchanged, do not emit an event
|
||||
pass
|
||||
else:
|
||||
self.logger.debug("changed %s -> %s", old_entry, new_entry)
|
||||
self.on_bookmark_changed(old_entry, new_entry)
|
||||
elif len(new) == 0:
|
||||
for removed in old:
|
||||
self.logger.debug("removed %s", removed)
|
||||
self.on_bookmark_removed(removed)
|
||||
elif len(old) == 0:
|
||||
for added in new:
|
||||
self.logger.debug("added %s", added)
|
||||
self.on_bookmark_added(added)
|
||||
else:
|
||||
old, new = subdivide(0, old, new)
|
||||
|
||||
assert len(old) == 0 or len(new) == 0
|
||||
|
||||
for removed in old:
|
||||
self.logger.debug("removed %s", removed)
|
||||
self.on_bookmark_removed(removed)
|
||||
|
||||
for added in new:
|
||||
self.logger.debug("added %s", added)
|
||||
self.on_bookmark_added(added)
|
||||
|
||||
self._bookmark_cache = new_bookmarks
|
||||
|
||||
async def get_bookmarks(self):
|
||||
"""
|
||||
Get the stored bookmarks from the server. Causes signals to be
|
||||
fired to reflect the changes.
|
||||
|
||||
:returns: a list of bookmarks
|
||||
"""
|
||||
async with self._lock:
|
||||
bookmarks = await self._get_bookmarks()
|
||||
self._diff_emit_update(bookmarks)
|
||||
return bookmarks
|
||||
|
||||
async def set_bookmarks(self, bookmarks):
|
||||
"""
|
||||
Store the sequence of bookmarks `bookmarks`.
|
||||
|
||||
Causes signals to be fired to reflect the changes.
|
||||
|
||||
.. note:: This should normally not be used. It does not
|
||||
mitigate the race condition between clients
|
||||
concurrently modifying the bookmarks and may lead to
|
||||
data loss. Use :meth:`add_bookmark`,
|
||||
:meth:`discard_bookmark` and :meth:`update_bookmark`
|
||||
instead. This method still has use-cases (modifying
|
||||
the bookmarklist at large, e.g. by syncing the
|
||||
remote store with local data).
|
||||
"""
|
||||
async with self._lock:
|
||||
await self._set_bookmarks(bookmarks)
|
||||
self._diff_emit_update(bookmarks)
|
||||
|
||||
async def sync(self):
|
||||
"""
|
||||
Sync the bookmarks between the local representation and the
|
||||
server.
|
||||
|
||||
This must be called periodically to assure that the signals
|
||||
are fired.
|
||||
"""
|
||||
await self.get_bookmarks()
|
||||
|
||||
async def add_bookmark(self, new_bookmark, *, max_retries=3):
|
||||
"""
|
||||
Add a bookmark and check whether it was successfully added to the
|
||||
bookmark list. Already existent bookmarks are not added twice.
|
||||
|
||||
:param new_bookmark: the bookmark to add
|
||||
:type new_bookmark: an instance of :class:`~bookmark_xso.Bookmark`
|
||||
:param max_retries: the number of retries if setting the bookmark
|
||||
fails
|
||||
:type max_retries: :class:`int`
|
||||
|
||||
:raises RuntimeError: if the bookmark is not in the bookmark list
|
||||
after `max_retries` retries.
|
||||
|
||||
After setting the bookmark it is checked, whether the bookmark
|
||||
is in the online storage, if it is not it is tried again at most
|
||||
`max_retries` times to add the bookmark. A :class:`RuntimeError`
|
||||
is raised if the bookmark could not be added successfully after
|
||||
`max_retries`.
|
||||
"""
|
||||
async with self._lock:
|
||||
bookmarks = await self._get_bookmarks()
|
||||
|
||||
try:
|
||||
modified_bookmarks = list(bookmarks)
|
||||
if new_bookmark not in bookmarks:
|
||||
modified_bookmarks.append(new_bookmark)
|
||||
await self._set_bookmarks(modified_bookmarks)
|
||||
|
||||
retries = 0
|
||||
bookmarks = await self._get_bookmarks()
|
||||
while retries < max_retries:
|
||||
if new_bookmark in bookmarks:
|
||||
break
|
||||
modified_bookmarks = list(bookmarks)
|
||||
modified_bookmarks.append(new_bookmark)
|
||||
await self._set_bookmarks(modified_bookmarks)
|
||||
bookmarks = await self._get_bookmarks()
|
||||
retries += 1
|
||||
|
||||
if new_bookmark not in bookmarks:
|
||||
raise RuntimeError("Could not add bookmark")
|
||||
|
||||
finally:
|
||||
self._diff_emit_update(bookmarks)
|
||||
|
||||
async def discard_bookmark(self, bookmark_to_remove, *, max_retries=3):
|
||||
"""
|
||||
Remove a bookmark and check it has been removed.
|
||||
|
||||
:param bookmark_to_remove: the bookmark to remove
|
||||
:type bookmark_to_remove: a :class:`~bookmark_xso.Bookmark` subclass.
|
||||
:param max_retries: the number of retries of removing the bookmark
|
||||
fails.
|
||||
:type max_retries: :class:`int`
|
||||
|
||||
:raises RuntimeError: if the bookmark is not removed from
|
||||
bookmark list after `max_retries`
|
||||
retries.
|
||||
|
||||
If there are multiple occurrences of the same bookmark exactly
|
||||
one is removed.
|
||||
|
||||
This does nothing if the bookmarks does not match an existing
|
||||
bookmark according to bookmark-equality.
|
||||
|
||||
After setting the bookmark it is checked, whether the bookmark
|
||||
is removed in the online storage, if it is not it is tried
|
||||
again at most `max_retries` times to remove the bookmark. A
|
||||
:class:`RuntimeError` is raised if the bookmark could not be
|
||||
removed successfully after `max_retries`.
|
||||
"""
|
||||
async with self._lock:
|
||||
bookmarks = await self._get_bookmarks()
|
||||
occurrences = bookmarks.count(bookmark_to_remove)
|
||||
|
||||
try:
|
||||
if not occurrences:
|
||||
return
|
||||
|
||||
modified_bookmarks = list(bookmarks)
|
||||
modified_bookmarks.remove(bookmark_to_remove)
|
||||
await self._set_bookmarks(modified_bookmarks)
|
||||
|
||||
retries = 0
|
||||
bookmarks = await self._get_bookmarks()
|
||||
new_occurences = bookmarks.count(bookmark_to_remove)
|
||||
while retries < max_retries:
|
||||
if new_occurences < occurrences:
|
||||
break
|
||||
modified_bookmarks = list(bookmarks)
|
||||
modified_bookmarks.remove(bookmark_to_remove)
|
||||
await self._set_bookmarks(modified_bookmarks)
|
||||
bookmarks = await self._get_bookmarks()
|
||||
new_occurences = bookmarks.count(bookmark_to_remove)
|
||||
retries += 1
|
||||
|
||||
if new_occurences >= occurrences:
|
||||
raise RuntimeError("Could not remove bookmark")
|
||||
finally:
|
||||
self._diff_emit_update(bookmarks)
|
||||
|
||||
async def update_bookmark(self, old, new, *, max_retries=3):
|
||||
"""
|
||||
Update a bookmark and check it was successful.
|
||||
|
||||
The bookmark matches an existing bookmark `old` according to
|
||||
bookmark equalitiy and replaces it by `new`. The bookmark
|
||||
`new` is added if no bookmark matching `old` exists.
|
||||
|
||||
:param old: the bookmark to replace
|
||||
:type bookmark_to_remove: a :class:`~bookmark_xso.Bookmark` subclass.
|
||||
:param new: the replacement bookmark
|
||||
:type bookmark_to_remove: a :class:`~bookmark_xso.Bookmark` subclass.
|
||||
:param max_retries: the number of retries of removing the bookmark
|
||||
fails.
|
||||
:type max_retries: :class:`int`
|
||||
|
||||
:raises RuntimeError: if the bookmark is not in the bookmark list
|
||||
after `max_retries` retries.
|
||||
|
||||
After replacing the bookmark it is checked, whether the
|
||||
bookmark `new` is in the online storage, if it is not it is
|
||||
tried again at most `max_retries` times to replace the
|
||||
bookmark. A :class:`RuntimeError` is raised if the bookmark
|
||||
could not be replaced successfully after `max_retries`.
|
||||
|
||||
.. note:: Do not modify a bookmark retrieved from the signals
|
||||
or from :meth:`get_bookmarks` to obtain the bookmark
|
||||
`new`, this will lead to data corruption as they are
|
||||
passed by reference. Instead use :func:`copy.copy`
|
||||
and modify the copy.
|
||||
|
||||
"""
|
||||
def replace_bookmark(bookmarks, old, new):
|
||||
modified_bookmarks = list(bookmarks)
|
||||
try:
|
||||
i = bookmarks.index(old)
|
||||
modified_bookmarks[i] = new
|
||||
except ValueError:
|
||||
modified_bookmarks.append(new)
|
||||
return modified_bookmarks
|
||||
|
||||
async with self._lock:
|
||||
bookmarks = await self._get_bookmarks()
|
||||
|
||||
try:
|
||||
await self._set_bookmarks(
|
||||
replace_bookmark(bookmarks, old, new)
|
||||
)
|
||||
|
||||
retries = 0
|
||||
bookmarks = await self._get_bookmarks()
|
||||
while retries < max_retries:
|
||||
if new in bookmarks:
|
||||
break
|
||||
await self._set_bookmarks(
|
||||
replace_bookmark(bookmarks, old, new)
|
||||
)
|
||||
bookmarks = await self._get_bookmarks()
|
||||
retries += 1
|
||||
|
||||
if new not in bookmarks:
|
||||
raise RuntimeError("Cold not update bookmark")
|
||||
finally:
|
||||
self._diff_emit_update(bookmarks)
|
||||
@@ -0,0 +1,262 @@
|
||||
########################################################################
|
||||
# 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/>.
|
||||
#
|
||||
########################################################################
|
||||
from abc import abstractproperty
|
||||
|
||||
import aioxmpp.private_xml as private_xml
|
||||
import aioxmpp.xso as xso
|
||||
|
||||
|
||||
from aioxmpp.utils import namespaces
|
||||
|
||||
|
||||
namespaces.xep0048 = "storage:bookmarks"
|
||||
|
||||
|
||||
class Bookmark(xso.XSO):
|
||||
"""
|
||||
A bookmark XSO abstract base class.
|
||||
|
||||
Every XSO class registered as child of :class:`Storage` must be
|
||||
a :class:`Bookmark` subclass.
|
||||
|
||||
Bookmarks must provide the following interface:
|
||||
|
||||
.. autoattribute:: primary
|
||||
|
||||
.. autoattribute:: secondary
|
||||
|
||||
.. autoattribute:: name
|
||||
|
||||
Equality is defined in terms of those properties:
|
||||
|
||||
.. automethod:: __eq__
|
||||
|
||||
It is highly recommended not to redefine :meth:`__eq__` in a
|
||||
subclass, if you do so make sure that the following axiom
|
||||
relating :meth:`__eq__`, :attr:`primary` and :attr:`secondary`
|
||||
holds::
|
||||
|
||||
(type(a) == type(b) and
|
||||
a.primary == b.primary and
|
||||
a.secondary == b.secondary)
|
||||
|
||||
if and only if::
|
||||
|
||||
a == b
|
||||
|
||||
Otherwise the generation of bookmark change signals is not
|
||||
guaranteed to be correct.
|
||||
"""
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Compare for equality by value and type.
|
||||
|
||||
The value of a bookmark must be fully determined by the values
|
||||
of the :attr:`primary` and :attr:`secondary` properties.
|
||||
|
||||
This is used for generating the bookmark list change signals
|
||||
and for the get-modify-set methods.
|
||||
"""
|
||||
return (type(self) == type(other) and
|
||||
self.primary == other.primary and
|
||||
self.secondary == other.secondary)
|
||||
|
||||
@abstractproperty
|
||||
def primary(self):
|
||||
"""
|
||||
Return the primary category of the bookmark.
|
||||
|
||||
The internal structure of the category is opaque to the code
|
||||
using it; only equality and hashing must be provided and
|
||||
operate by value. It is recommended that this be either a
|
||||
single datum (e.g. a string or JID) or a tuple of data items.
|
||||
|
||||
Together with the type and :attr:`secondary` this must *fully*
|
||||
determine the value of the bookmark.
|
||||
|
||||
This is used in the computation of the change
|
||||
signals. Bookmarks with different type or :attr:`primary`
|
||||
keys cannot be identified as changed from/to one another.
|
||||
"""
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
@abstractproperty
|
||||
def secondary(self):
|
||||
"""
|
||||
Return the tuple of secondary categories of the bookmark.
|
||||
|
||||
Together with the type and :attr:`primary` they must *fully*
|
||||
determine the value of the bookmark.
|
||||
|
||||
This is used in the computation of the change signals. The
|
||||
categories in the tuple are ordered in decreasing precedence,
|
||||
when calculating which bookmarks have changed the ones which
|
||||
mismatch in the category with the lowest precedence are
|
||||
grouped together.
|
||||
|
||||
The length of the tuple must be the same for all bookmarks of
|
||||
a type.
|
||||
"""
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
@abstractproperty
|
||||
def name(self):
|
||||
"""
|
||||
The human-readable label or description of the bookmark.
|
||||
"""
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
|
||||
class Conference(Bookmark):
|
||||
"""
|
||||
An bookmark for a groupchat.
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
The name of the bookmark.
|
||||
|
||||
.. attribute:: jid
|
||||
|
||||
The jid under which the groupchat is accessible.
|
||||
|
||||
.. attribute:: autojoin
|
||||
|
||||
Whether to join automatically, when the client starts.
|
||||
|
||||
.. attribute:: nick
|
||||
|
||||
The nick to use in the groupchat.
|
||||
|
||||
.. attribute:: password
|
||||
|
||||
The password used to access the groupchat.
|
||||
"""
|
||||
|
||||
TAG = (namespaces.xep0048, "conference")
|
||||
|
||||
autojoin = xso.Attr(tag="autojoin", type_=xso.Bool(), default=False)
|
||||
jid = xso.Attr(tag="jid", type_=xso.JID())
|
||||
name = xso.Attr(tag="name", type_=xso.String(), default=None)
|
||||
|
||||
nick = xso.ChildText(
|
||||
(namespaces.xep0048, "nick"),
|
||||
default=None
|
||||
)
|
||||
password = xso.ChildText(
|
||||
(namespaces.xep0048, "password"),
|
||||
default=None
|
||||
)
|
||||
|
||||
def __init__(self, name, jid, *, autojoin=False, nick=None, password=None):
|
||||
self.autojoin = autojoin
|
||||
self.jid = jid
|
||||
self.name = name
|
||||
self.nick = nick
|
||||
self.password = password
|
||||
|
||||
def __repr__(self):
|
||||
return "Conference({!r}, {!r}, autojoin={!r}, " \
|
||||
"nick={!r}, password{!r})".\
|
||||
format(self.name, self.jid, self.autojoin, self.nick,
|
||||
self.password)
|
||||
|
||||
@property
|
||||
def primary(self):
|
||||
return self.jid
|
||||
|
||||
@property
|
||||
def secondary(self):
|
||||
return (self.name, self.nick, self.password, self.autojoin)
|
||||
|
||||
|
||||
class URL(Bookmark):
|
||||
"""
|
||||
An URL bookmark.
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
The name of the bookmark.
|
||||
|
||||
.. attribute:: url
|
||||
|
||||
The URL the bookmark saves.
|
||||
"""
|
||||
TAG = (namespaces.xep0048, "url")
|
||||
|
||||
name = xso.Attr(tag="name", type_=xso.String(), default=None)
|
||||
# XXX: we might want to use a URL type once we have one
|
||||
url = xso.Attr(tag="url", type_=xso.String())
|
||||
|
||||
def __init__(self, name, url):
|
||||
self.name = name
|
||||
self.url = url
|
||||
|
||||
def __repr__(self):
|
||||
return "URL({!r}, {!r})".format(self.name, self.url)
|
||||
|
||||
@property
|
||||
def primary(self):
|
||||
return self.url
|
||||
|
||||
@property
|
||||
def secondary(self):
|
||||
return (self.name,)
|
||||
|
||||
|
||||
@private_xml.Query.as_payload_class
|
||||
class Storage(xso.XSO):
|
||||
"""
|
||||
The container for storing bookmarks.
|
||||
|
||||
.. attribute:: bookmarks
|
||||
|
||||
A :class:`~xso.XSOList` of bookmarks.
|
||||
"""
|
||||
|
||||
TAG = (namespaces.xep0048, "storage")
|
||||
|
||||
bookmarks = xso.ChildList([URL, Conference])
|
||||
|
||||
|
||||
def as_bookmark_class(xso_class):
|
||||
"""
|
||||
Decorator to register `xso_class` as a custom bookmark class.
|
||||
|
||||
This is necessary to store and retrieve such bookmarks.
|
||||
The registered class must be a subclass of the abstract base class
|
||||
:class:`Bookmark`.
|
||||
|
||||
:raises TypeError: if `xso_class` is not a subclass of :class:`Bookmark`.
|
||||
"""
|
||||
|
||||
if not issubclass(xso_class, Bookmark):
|
||||
raise TypeError(
|
||||
"Classes registered as bookmark types must be Bookmark subclasses"
|
||||
)
|
||||
|
||||
Storage.register_child(
|
||||
Storage.bookmarks,
|
||||
xso_class
|
||||
)
|
||||
|
||||
return xso_class
|
||||
Reference in New Issue
Block a user