plugin XEP-0048, XEP-0402; CLI (bookmarks): implement XEP-0402 (PEP Native Bookmarks):
- Former bookmarks implementation is now labeled as "legacy". - XEP-0402 is now used for bookmarks when relevant namespaces are found, and it fallbacks to legacy XEP-0048/XEP-0049 bookmarks otherwise. - CLI legacy bookmark commands have been moved to `bookmarks legacy` - CLI bookmarks commands now use the new XEP-0402 (with fallback to legacy one automatically used if necessary).
This commit is contained in:
parent
0c27fef2ac
commit
06a1eef720
11 changed files with 858 additions and 165 deletions
|
|
@ -135,7 +135,7 @@ class Discovery(object):
|
|||
|
||||
@param feature: feature namespace
|
||||
@param jid_: jid of the target, or None for profile's server
|
||||
@param node(unicode): optional node to use for disco request
|
||||
@param node: optional node to use for disco request
|
||||
@return: a Deferred which fire a boolean (True if feature is available)
|
||||
"""
|
||||
disco_infos = await self.get_infos(client, jid_, node)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
# SAT plugin for Bookmarks (xep-0048)
|
||||
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
|
||||
# Libervia plugin for Bookmarks (xep-0048)
|
||||
# Copyright (C) 2009-2024 Jérôme Poisson (goffi@goffi.org)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
|
|
@ -71,28 +71,30 @@ class XEP_0048(object):
|
|||
self.__selected_id = host.register_callback(
|
||||
self._bookmark_selected_cb, with_data=True
|
||||
)
|
||||
# XXX: We're transitionning to XEP-0402, so we mark bridge method as "legacy"
|
||||
# here.
|
||||
host.bridge.add_method(
|
||||
"bookmarks_list",
|
||||
"bookmarks_legacy_list",
|
||||
".plugin",
|
||||
in_sign="sss",
|
||||
out_sign="a{sa{sa{ss}}}",
|
||||
method=self._bookmarks_list,
|
||||
method=self.bookmarks_list,
|
||||
async_=True,
|
||||
)
|
||||
host.bridge.add_method(
|
||||
"bookmarks_remove",
|
||||
"bookmarks_legacy_remove",
|
||||
".plugin",
|
||||
in_sign="ssss",
|
||||
out_sign="",
|
||||
method=self._bookmarks_remove,
|
||||
method=self.bookmarks_remove,
|
||||
async_=True,
|
||||
)
|
||||
host.bridge.add_method(
|
||||
"bookmarks_add",
|
||||
"bookmarks_legacy_add",
|
||||
".plugin",
|
||||
in_sign="ssa{ss}ss",
|
||||
out_sign="",
|
||||
method=self._bookmarks_add,
|
||||
method=self.bookmarks_add,
|
||||
async_=True,
|
||||
)
|
||||
try:
|
||||
|
|
@ -419,7 +421,12 @@ class XEP_0048(object):
|
|||
if storage_type == "pubsub":
|
||||
raise NotImplementedError
|
||||
|
||||
def _bookmarks_list(self, type_, storage_location, profile_key=C.PROF_KEY_NONE):
|
||||
def bookmarks_list(
|
||||
self,
|
||||
type_: str,
|
||||
storage_location: str,
|
||||
profile_key: str = C.PROF_KEY_NONE
|
||||
) -> defer.Deferred[dict]:
|
||||
"""Return stored bookmarks
|
||||
|
||||
@param type_: bookmark type, one of:
|
||||
|
|
@ -458,7 +465,8 @@ class XEP_0048(object):
|
|||
if storage_location in ("all", _storage_location):
|
||||
ret[_storage_location] = {}
|
||||
if _storage_location in ("private",):
|
||||
# we update distant bookmarks, just in case an other client added something
|
||||
# we update distant bookmarks, just in case an other client added
|
||||
# something
|
||||
d = self._get_server_bookmarks(_storage_location, client.profile)
|
||||
else:
|
||||
d = defer.succeed(None)
|
||||
|
|
@ -467,7 +475,7 @@ class XEP_0048(object):
|
|||
|
||||
return ret_d
|
||||
|
||||
def _bookmarks_remove(
|
||||
def bookmarks_remove(
|
||||
self, type_, location, storage_location, profile_key=C.PROF_KEY_NONE
|
||||
):
|
||||
"""Return stored bookmarks
|
||||
|
|
@ -487,7 +495,7 @@ class XEP_0048(object):
|
|||
location = jid.JID(location)
|
||||
return self.remove_bookmark(type_, location, storage_location, profile_key)
|
||||
|
||||
def _bookmarks_add(
|
||||
def bookmarks_add(
|
||||
self, type_, location, data, storage_type="auto", profile_key=C.PROF_KEY_NONE
|
||||
):
|
||||
if type_ == XEP_0048.MUC_TYPE:
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ class XEP_0060(object):
|
|||
ID_SINGLETON = "current"
|
||||
EXTRA_PUBLISH_OPTIONS = "publish_options"
|
||||
EXTRA_ON_PRECOND_NOT_MET = "on_precondition_not_met"
|
||||
EXTRA_AUTOCREATE = "autocreate"
|
||||
# extra disco needed for RSM, cf. XEP-0060 § 6.5.4
|
||||
DISCO_RSM = "http://jabber.org/protocol/pubsub#rsm"
|
||||
|
||||
|
|
@ -608,6 +609,8 @@ class XEP_0060(object):
|
|||
* publish_without_options: re-publish without the publish-options.
|
||||
A warning will be logged showing that the publish-options could not
|
||||
be used
|
||||
- self.EXTRA_AUTOCREATE(bool): Create the node if it's not found, and the
|
||||
service doesn't do autocreate itself.
|
||||
@return: ids of the created items
|
||||
"""
|
||||
if extra is None:
|
||||
|
|
@ -631,7 +634,25 @@ class XEP_0060(object):
|
|||
sender=sender,
|
||||
)
|
||||
except error.StanzaError as e:
|
||||
if (
|
||||
if e.condition == "item-not-found":
|
||||
if extra.get(self.EXTRA_AUTOCREATE, False):
|
||||
# Autocreate is requested, we create the requested node.
|
||||
await self.createNode(
|
||||
client, service, nodeIdentifier, publish_options
|
||||
)
|
||||
# And we try again.
|
||||
iq_result = await self.publish(
|
||||
client,
|
||||
service,
|
||||
nodeIdentifier,
|
||||
parsed_items,
|
||||
options=publish_options,
|
||||
sender=sender,
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
|
||||
elif (
|
||||
e.condition == "conflict"
|
||||
and e.appCondition
|
||||
and e.appCondition.name == "precondition-not-met"
|
||||
|
|
@ -690,15 +711,15 @@ class XEP_0060(object):
|
|||
client: SatXMPPEntity,
|
||||
service: jid.JID,
|
||||
nodeIdentifier: str,
|
||||
items: Optional[List[domish.Element]] = None,
|
||||
options: Optional[dict] = None,
|
||||
sender: Optional[jid.JID] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
items: list[pubsub.Item]|None = None,
|
||||
options: dict|None = None,
|
||||
sender: jid.JID|None = None,
|
||||
extra: dict[str, Any]|None = None,
|
||||
) -> domish.Element:
|
||||
"""Publish pubsub items
|
||||
|
||||
@param sender: sender of the request,
|
||||
client.jid will be used if nto set
|
||||
client.jid will be used if not set
|
||||
@param extra: extra data
|
||||
not used directly by ``publish``, but may be used in triggers
|
||||
@return: IQ result stanza
|
||||
|
|
@ -786,26 +807,26 @@ class XEP_0060(object):
|
|||
async def get_items(
|
||||
self,
|
||||
client: SatXMPPEntity,
|
||||
service: Optional[jid.JID],
|
||||
service: jid.JID|None,
|
||||
node: str,
|
||||
max_items: Optional[int] = None,
|
||||
item_ids: Optional[List[str]] = None,
|
||||
sub_id: Optional[str] = None,
|
||||
rsm_request: Optional[rsm.RSMRequest] = None,
|
||||
extra: Optional[dict] = None,
|
||||
) -> Tuple[List[domish.Element], dict]:
|
||||
max_items: int|None = None,
|
||||
item_ids: list[str]|None = None,
|
||||
sub_id: str|None = None,
|
||||
rsm_request: rsm.RSMRequest|None = None,
|
||||
extra: dict|None = None,
|
||||
) -> tuple[list[domish.Element], dict]:
|
||||
"""Retrieve pubsub items from a node.
|
||||
|
||||
@param service (JID, None): pubsub service.
|
||||
@param node (str): node id.
|
||||
@param max_items (int): optional limit on the number of retrieved items.
|
||||
@param service: pubsub service.
|
||||
@param node: node id.
|
||||
@param max_items: optional limit on the number of retrieved items.
|
||||
@param item_ids (list[str]): identifiers of the items to be retrieved (can't be
|
||||
used with rsm_request). If requested items don't exist, they won't be
|
||||
returned, meaning that we can have an empty list as result (NotFound
|
||||
exception is NOT raised).
|
||||
@param sub_id (str): optional subscription identifier.
|
||||
@param rsm_request (rsm.RSMRequest): RSM request data
|
||||
@return: a deferred couple (list[dict], dict) containing:
|
||||
@param sub_id : optional subscription identifier.
|
||||
@param rsm_request: RSM request data
|
||||
@return: a deferred tuple containing:
|
||||
- list of items
|
||||
- metadata with the following keys:
|
||||
- rsm_first, rsm_last, rsm_count, rsm_index: first, last, count and index
|
||||
|
|
@ -980,11 +1001,11 @@ class XEP_0060(object):
|
|||
|
||||
def createNode(
|
||||
self,
|
||||
client: SatXMPPClient,
|
||||
client: SatXMPPEntity,
|
||||
service: jid.JID,
|
||||
nodeIdentifier: Optional[str] = None,
|
||||
options: Optional[Dict[str, str]] = None,
|
||||
) -> str:
|
||||
) -> defer.Deferred[str]:
|
||||
"""Create a new node
|
||||
|
||||
@param service: PubSub service,
|
||||
|
|
@ -1234,7 +1255,7 @@ class XEP_0060(object):
|
|||
|
||||
def retract_items(
|
||||
self,
|
||||
client: SatXMPPClient,
|
||||
client: SatXMPPEntity,
|
||||
service: jid.JID,
|
||||
nodeIdentifier: str,
|
||||
itemIdentifiers: Iterable[str],
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Optional, Callable
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from libervia.backend.core.core_types import SatXMPPEntity
|
||||
from libervia.backend.core.i18n import _
|
||||
from libervia.backend.core import exceptions
|
||||
from libervia.backend.core.constants import Const as C
|
||||
|
|
@ -28,6 +31,7 @@ from twisted.words.xish import domish
|
|||
from wokkel import disco, pubsub
|
||||
from wokkel.formats import Mood
|
||||
from libervia.backend.tools.common import data_format
|
||||
from libervia.backend.tools import utils
|
||||
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
|
@ -74,12 +78,15 @@ class XEP_0163(object):
|
|||
disco_info.extend(list(map(disco.DiscoFeature, self.pep_events)))
|
||||
return True
|
||||
|
||||
def log_error(self, failure_: Failure) -> None:
|
||||
log.error(f"Failed to call callback: {failure_}")
|
||||
|
||||
def add_pep_event(
|
||||
self,
|
||||
event_type: Optional[str],
|
||||
event_type: str | None,
|
||||
node: str,
|
||||
in_callback: Callable,
|
||||
out_callback: Optional[Callable] = None,
|
||||
in_callback: Callable[[pubsub.ItemsEvent, str], None],
|
||||
out_callback: Callable | None = None,
|
||||
notify: bool = True,
|
||||
) -> None:
|
||||
"""Add a Personal Eventing Protocol event manager
|
||||
|
|
@ -90,7 +97,8 @@ class XEP_0163(object):
|
|||
@param node: namespace of the node (e.g. http://jabber.org/protocol/mood
|
||||
for User Mood)
|
||||
@param in_callback: method to call when this event occur
|
||||
the callable will be called with (itemsEvent, profile) as arguments
|
||||
the callable will be called with (itemsEvent, profile) as arguments.
|
||||
Can be blocking or async.
|
||||
@param out_callback: method to call when we want to publish this
|
||||
event (must return a deferred)
|
||||
the callable will be called when send_pep_event is called
|
||||
|
|
@ -107,19 +115,20 @@ class XEP_0163(object):
|
|||
if notify:
|
||||
self.pep_events.add(node + "+notify")
|
||||
|
||||
def filter_pep_event(client, itemsEvent):
|
||||
def filter_pep_event(client: SatXMPPEntity, itemsEvent: pubsub.ItemsEvent):
|
||||
"""Ignore messages which are not coming from PEP (i.e. a bare jid)
|
||||
|
||||
@param itemsEvent(pubsub.ItemsEvent): pubsub event
|
||||
@param itemsEvent: pubsub event
|
||||
"""
|
||||
if not itemsEvent.sender.user or itemsEvent.sender.resource:
|
||||
log.debug(
|
||||
log.warning(
|
||||
"ignoring non PEP event from {} (profile={})".format(
|
||||
itemsEvent.sender.full(), client.profile
|
||||
)
|
||||
)
|
||||
return
|
||||
in_callback(itemsEvent, client.profile)
|
||||
d = utils.as_deferred(in_callback, itemsEvent, client.profile)
|
||||
d.addErrback(self.log_error)
|
||||
|
||||
self.host.plugins["XEP-0060"].add_managed_node(node, items_cb=filter_pep_event)
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,10 @@ class XEP_0376:
|
|||
sub_jid: Optional[jid.JID] = None,
|
||||
options: Optional[dict] = None,
|
||||
) -> Tuple[bool, Optional[pubsub.Subscription]]:
|
||||
if not await self.host.memory.disco.has_feature(client, NS_PAM) or client.is_component:
|
||||
if (
|
||||
not await self.host.memory.disco.has_feature(client, NS_PAM)
|
||||
or client.is_component
|
||||
):
|
||||
return True, None
|
||||
|
||||
await self._sub_request(client, service, nodeIdentifier, sub_jid, options, True)
|
||||
|
|
@ -127,7 +130,10 @@ class XEP_0376:
|
|||
subscriptionIdentifier: Optional[str],
|
||||
sender: Optional[jid.JID] = None,
|
||||
) -> bool:
|
||||
if not await self.host.memory.disco.has_feature(client, NS_PAM) or client.is_component:
|
||||
if (
|
||||
not await self.host.memory.disco.has_feature(client, NS_PAM)
|
||||
or client.is_component
|
||||
):
|
||||
return True
|
||||
await self._sub_request(client, service, nodeIdentifier, sub_jid, None, False)
|
||||
return False
|
||||
|
|
|
|||
443
libervia/backend/plugins/plugin_xep_0402.py
Normal file
443
libervia/backend/plugins/plugin_xep_0402.py
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Libervia plugin to handle chat room bookmarks via PEP
|
||||
# Copyright (C) 2009-2024 Jérôme Poisson (goffi@goffi.org)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Iterator, Self, cast
|
||||
|
||||
from pydantic import BaseModel, Field, RootModel
|
||||
from twisted.internet import defer
|
||||
from twisted.words.protocols.jabber import jid
|
||||
from twisted.words.xish import domish
|
||||
from wokkel import pubsub
|
||||
|
||||
from libervia.backend.core import exceptions
|
||||
from libervia.backend.core.constants import Const as C
|
||||
from libervia.backend.core.core_types import SatXMPPEntity
|
||||
from libervia.backend.core.i18n import _
|
||||
from libervia.backend.core.log import getLogger
|
||||
from libervia.backend.models.types import DomishElementType, JIDType
|
||||
from libervia.backend.plugins.plugin_xep_0045 import XEP_0045
|
||||
from libervia.backend.plugins.plugin_xep_0048 import XEP_0048
|
||||
from libervia.backend.plugins.plugin_xep_0060 import XEP_0060
|
||||
from libervia.backend.tools import utils
|
||||
from libervia.backend.tools.common import data_format
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
PLUGIN_INFO = {
|
||||
C.PI_NAME: "PEP Native Bookmarks",
|
||||
C.PI_IMPORT_NAME: "XEP-0402",
|
||||
C.PI_TYPE: "XEP",
|
||||
C.PI_MODES: C.PLUG_MODE_BOTH,
|
||||
C.PI_PROTOCOLS: [],
|
||||
C.PI_DEPENDENCIES: ["XEP-0048", "XEP-0060", "XEP-0163", "XEP-0045"],
|
||||
C.PI_RECOMMENDATIONS: [],
|
||||
C.PI_MAIN: "XEP_0402",
|
||||
C.PI_HANDLER: "no",
|
||||
C.PI_DESCRIPTION: _("""Bookmark chat rooms, and handle their joined state."""),
|
||||
}
|
||||
|
||||
NS_BOOKMARKS2 = "urn:xmpp:bookmarks:1"
|
||||
NS_BOOKMARKS2_COMPAT = f"{NS_BOOKMARKS2}#compat"
|
||||
|
||||
|
||||
class Conference(BaseModel):
|
||||
"""
|
||||
Model for conference data.
|
||||
"""
|
||||
|
||||
autojoin: bool = Field(
|
||||
False,
|
||||
description="Whether the client should automatically join the conference room on "
|
||||
"login.",
|
||||
)
|
||||
name: str | None = Field(
|
||||
None, description="A friendly name for the bookmark, specified by the user."
|
||||
)
|
||||
nick: str | None = Field(
|
||||
None, description="The user's preferred roomnick for the chatroom."
|
||||
)
|
||||
password: str | None = Field(
|
||||
None, description="A password used to access the chatroom."
|
||||
)
|
||||
extensions: list[DomishElementType] = Field(
|
||||
default_factory=list,
|
||||
description="A set of child elements (of potentially any namespace).",
|
||||
)
|
||||
|
||||
def set_attributes(self, conference_elt: domish.Element) -> None:
|
||||
"""Set <conference> element attributes from this instance's data."""
|
||||
if self.autojoin:
|
||||
conference_elt["autojoin"] = "true" if self.autojoin else "false"
|
||||
if self.name:
|
||||
conference_elt["name"] = self.name
|
||||
|
||||
def set_children(self, conference_elt: domish.Element) -> None:
|
||||
"""Set <conference> element children from this instance's data."""
|
||||
if self.nick:
|
||||
nick_elt = conference_elt.addElement((NS_BOOKMARKS2, "nick"))
|
||||
nick_elt.addContent(self.nick)
|
||||
if self.password:
|
||||
password_elt = conference_elt.addElement((NS_BOOKMARKS2, "password"))
|
||||
password_elt.addContent(self.password)
|
||||
for extension in self.extensions:
|
||||
conference_elt.addChild(extension)
|
||||
|
||||
@classmethod
|
||||
def from_element(cls, conference_elt: domish.Element) -> Self:
|
||||
"""
|
||||
Create a Conference instance from a <conference> element or its parent.
|
||||
|
||||
@param conference_elt: The <conference> element or a parent element.
|
||||
@return: Conference instance.
|
||||
@raise exceptions.NotFound: If the <conference> element is not found.
|
||||
"""
|
||||
if conference_elt.uri != NS_BOOKMARKS2 or conference_elt.name != "conference":
|
||||
child_conference_elt = next(
|
||||
conference_elt.elements(NS_BOOKMARKS2, "conference"), None
|
||||
)
|
||||
if child_conference_elt is None:
|
||||
raise exceptions.NotFound("<conference> element not found")
|
||||
else:
|
||||
conference_elt = child_conference_elt
|
||||
kwargs = {}
|
||||
if conference_elt.hasAttribute("autojoin"):
|
||||
kwargs["autojoin"] = conference_elt["autojoin"] == "true"
|
||||
if conference_elt.hasAttribute("name"):
|
||||
kwargs["name"] = conference_elt["name"]
|
||||
nick_elt = next(conference_elt.elements(NS_BOOKMARKS2, "nick"), None)
|
||||
if nick_elt:
|
||||
kwargs["nick"] = str(nick_elt)
|
||||
password_elt = next(conference_elt.elements(NS_BOOKMARKS2, "password"), None)
|
||||
if password_elt:
|
||||
kwargs["password"] = str(password_elt)
|
||||
kwargs["extensions"] = [
|
||||
child for child in conference_elt.elements() if child.uri != NS_BOOKMARKS2
|
||||
]
|
||||
return cls(**kwargs)
|
||||
|
||||
def to_element(self) -> domish.Element:
|
||||
"""Build the <conference> element from this instance's data.
|
||||
|
||||
@return: <conference> element.
|
||||
"""
|
||||
conference_elt = domish.Element((NS_BOOKMARKS2, "conference"))
|
||||
self.set_attributes(conference_elt)
|
||||
self.set_children(conference_elt)
|
||||
return conference_elt
|
||||
|
||||
|
||||
class Bookmarks(RootModel):
|
||||
root: dict[JIDType, Conference]
|
||||
|
||||
def items(self):
|
||||
return self.root.items()
|
||||
|
||||
def __dict__(self) -> dict[JIDType, Conference]: # type: ignore
|
||||
return self.root
|
||||
|
||||
def __iter__(self) -> Iterator[JIDType]: # type: ignore
|
||||
return iter(self.root)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.root[item]
|
||||
|
||||
@classmethod
|
||||
def from_elements(cls, items_elt: list[domish.Element]) -> Self:
|
||||
"""Convert list of items to instance of Bookmarks.
|
||||
|
||||
@param items_elt: list of <item> elements from boorkmarks node.
|
||||
"""
|
||||
bookmarks = {}
|
||||
|
||||
for item_elt in items_elt:
|
||||
try:
|
||||
bookmark_jid = jid.JID(item_elt["id"])
|
||||
except RuntimeError as e:
|
||||
log.warning(f"Can't parse bookmark jid {e}: {item_elt.toXml()}")
|
||||
continue
|
||||
try:
|
||||
conference = Conference.from_element(item_elt)
|
||||
except exceptions.NotFound:
|
||||
log.warning(f"Can't find conference data in bookmark: {item_elt}")
|
||||
else:
|
||||
bookmarks[bookmark_jid] = conference
|
||||
|
||||
return cls(bookmarks)
|
||||
|
||||
|
||||
class XEP_0402:
|
||||
namespace = NS_BOOKMARKS2
|
||||
|
||||
def __init__(self, host):
|
||||
log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization")
|
||||
self.host = host
|
||||
self._legacy = cast(XEP_0048, host.plugins["XEP-0048"])
|
||||
self._p = cast(XEP_0060, host.plugins["XEP-0060"])
|
||||
self._muc = cast(XEP_0045, host.plugins["XEP-0045"])
|
||||
host.bridge.add_method(
|
||||
"bookmark_get",
|
||||
".plugin",
|
||||
in_sign="ss",
|
||||
out_sign="s",
|
||||
method=self._bookmark_get,
|
||||
async_=True,
|
||||
)
|
||||
host.bridge.add_method(
|
||||
"bookmarks_list",
|
||||
".plugin",
|
||||
in_sign="ss",
|
||||
out_sign="s",
|
||||
method=self._bookmarks_list,
|
||||
async_=True,
|
||||
)
|
||||
host.bridge.add_method(
|
||||
"bookmark_remove",
|
||||
".plugin",
|
||||
in_sign="ss",
|
||||
out_sign="",
|
||||
method=self._bookmark_remove,
|
||||
async_=True,
|
||||
)
|
||||
host.bridge.add_method(
|
||||
"bookmarks_set",
|
||||
".plugin",
|
||||
in_sign="ss",
|
||||
out_sign="",
|
||||
method=self._bookmarks_set,
|
||||
async_=True,
|
||||
)
|
||||
host.plugins["XEP-0163"].add_pep_event(
|
||||
None, NS_BOOKMARKS2, self._on_bookmark_event
|
||||
)
|
||||
host.register_namespace("bookmarks2", NS_BOOKMARKS2)
|
||||
|
||||
async def _on_bookmark_event(
|
||||
self, items_events: pubsub.ItemsEvent, profile: str
|
||||
) -> None:
|
||||
client = self.host.get_client(profile)
|
||||
if items_events.sender != client.jid.userhostJID():
|
||||
log.warning(
|
||||
"Bookmark event Unexpectedly send for another account "
|
||||
f"({items_events.sender})."
|
||||
)
|
||||
else:
|
||||
for item_elt in items_events.items:
|
||||
try:
|
||||
room_jid = jid.JID(item_elt["id"])
|
||||
except Exception as e:
|
||||
log.warning(
|
||||
f'Ignoring bookmark due to invalid JID in "id" ({e}): '
|
||||
f"{item_elt.toXml()}"
|
||||
)
|
||||
continue
|
||||
try:
|
||||
conference = Conference.from_element(item_elt)
|
||||
except exceptions.NotFound:
|
||||
log.warning("Ignoring invalid bookmark element: {item_elt.toXml()}")
|
||||
except Exception:
|
||||
log.exception("Can't parse bookmark item: {item_elt.toXml()}")
|
||||
else:
|
||||
if conference.autojoin:
|
||||
await self._muc.join(
|
||||
client,
|
||||
room_jid,
|
||||
conference.nick,
|
||||
{"password": conference.password},
|
||||
)
|
||||
else:
|
||||
await self._muc.leave(client, room_jid)
|
||||
|
||||
@utils.ensure_deferred
|
||||
async def _bookmark_get(self, bookmark_jid: str, profile: str) -> str:
|
||||
"""List current boorkmarks.
|
||||
|
||||
@param extra_s: Serialised extra.
|
||||
Reserved for future use.
|
||||
@param profile: Profile to use.
|
||||
"""
|
||||
client = self.host.get_client(profile)
|
||||
conference = await self.get(client, jid.JID(bookmark_jid))
|
||||
return conference.model_dump_json(exclude_none=True)
|
||||
|
||||
async def get(self, client: SatXMPPEntity, bookmark_jid: jid.JID) -> Conference:
|
||||
"""Helper method to get a single bookmark.
|
||||
|
||||
@param client: profile session.
|
||||
@bookmark_jid: JID of the boorkmark to get.
|
||||
@return: Conference instance.
|
||||
"""
|
||||
pep_jid = client.jid.userhostJID()
|
||||
if await self.host.memory.disco.has_feature(
|
||||
client, NS_BOOKMARKS2_COMPAT, pep_jid
|
||||
):
|
||||
items, __ = await self._p.get_items(
|
||||
client,
|
||||
client.jid.userhostJID(),
|
||||
NS_BOOKMARKS2,
|
||||
item_ids=[bookmark_jid.full()],
|
||||
)
|
||||
return Conference.from_element(items[0])
|
||||
else:
|
||||
# No compatibility layer, we use legacy bookmarks.
|
||||
bookmarks = await self.list(client)
|
||||
return bookmarks[bookmark_jid]
|
||||
|
||||
def _bookmark_remove(self, bookmark_jid: str, profile: str) -> defer.Deferred[None]:
|
||||
d = defer.ensureDeferred(
|
||||
self.remove(self.host.get_client(profile), jid.JID(bookmark_jid))
|
||||
)
|
||||
return d
|
||||
|
||||
async def remove(self, client: SatXMPPEntity, bookmark_jid: jid.JID) -> None:
|
||||
"""Helper method to delete an existing bookmark.
|
||||
|
||||
@param client: Profile session.
|
||||
@param bookmark_jid: Bookmark to delete.
|
||||
"""
|
||||
pep_jid = client.jid.userhostJID()
|
||||
|
||||
if await self.host.memory.disco.has_feature(
|
||||
client, NS_BOOKMARKS2_COMPAT, pep_jid
|
||||
):
|
||||
await self._p.retract_items(
|
||||
client, client.jid.userhostJID(), NS_BOOKMARKS2, [bookmark_jid.full()]
|
||||
)
|
||||
else:
|
||||
log.debug(
|
||||
f"[{client.profile}] No compatibility layer found, we use legacy "
|
||||
"bookmarks."
|
||||
)
|
||||
await self._legacy.remove_bookmark(
|
||||
self._legacy.MUC_TYPE, bookmark_jid, "private", client.profile
|
||||
)
|
||||
|
||||
@utils.ensure_deferred
|
||||
async def _bookmarks_list(self, extra_s: str, profile: str) -> str:
|
||||
"""List current boorkmarks.
|
||||
|
||||
@param extra_s: Serialised extra.
|
||||
Reserved for future use.
|
||||
@param profile: Profile to use.
|
||||
"""
|
||||
client = self.host.get_client(profile)
|
||||
extra = data_format.deserialise(extra_s)
|
||||
bookmarks = await self.list(client, extra)
|
||||
return bookmarks.model_dump_json(exclude_none=True)
|
||||
|
||||
async def list(self, client: SatXMPPEntity, extra: dict | None = None) -> Bookmarks:
|
||||
"""List bookmarks.
|
||||
|
||||
If there is a compatibility layer announced, Bookmarks2 will be used, otherwise
|
||||
legacy bookmarks will be used.
|
||||
@param client: Client session.
|
||||
@param extra: extra dat)
|
||||
@return: bookmarks.
|
||||
"""
|
||||
pep_jid = client.jid.userhostJID()
|
||||
|
||||
if await self.host.memory.disco.has_feature(
|
||||
client, NS_BOOKMARKS2_COMPAT, pep_jid
|
||||
):
|
||||
items, __ = await self._p.get_items(
|
||||
client, client.jid.userhostJID(), NS_BOOKMARKS2
|
||||
)
|
||||
return Bookmarks.from_elements(items)
|
||||
else:
|
||||
# There is no compatibility layer on the PEP server, so we use legacy
|
||||
# bookmarks as recommended at
|
||||
# https://docs.modernxmpp.org/client/groupchat/#bookmarks
|
||||
log.debug(
|
||||
f"[{client.profile}] No compatibility layer found, we use legacy "
|
||||
"bookmarks."
|
||||
)
|
||||
legacy_data = await self._legacy.bookmarks_list(
|
||||
self._legacy.MUC_TYPE, "private", client.profile
|
||||
)
|
||||
private_bookmarks = legacy_data["private"]
|
||||
bookmarks_dict = {}
|
||||
for jid, bookmark_data in private_bookmarks.items():
|
||||
autojoin = C.bool(bookmark_data.get("autojoin", C.BOOL_FALSE))
|
||||
name = bookmark_data.get("name")
|
||||
nick = bookmark_data.get("nick")
|
||||
password = bookmark_data.get("password")
|
||||
conference = Conference(
|
||||
autojoin=autojoin, name=name, nick=nick, password=password
|
||||
)
|
||||
bookmarks_dict[jid] = conference
|
||||
return Bookmarks(bookmarks_dict)
|
||||
|
||||
@utils.ensure_deferred
|
||||
async def _bookmarks_set(self, bookmarks_raw: str, profile: str) -> None:
|
||||
"""Add or update one or more bookmarks.
|
||||
|
||||
@param bookmarks_raw: serialised bookmark.
|
||||
It must deserialise to a dict mapping from bookmark JID to Conference data.
|
||||
@param profile: Profile to use.
|
||||
"""
|
||||
client = self.host.get_client(profile)
|
||||
bookmarks = Bookmarks.model_validate_json(bookmarks_raw)
|
||||
pep_jid = client.jid.userhostJID()
|
||||
|
||||
if await self.host.memory.disco.has_feature(
|
||||
client, NS_BOOKMARKS2_COMPAT, pep_jid
|
||||
):
|
||||
bookmark_items = []
|
||||
for bookmark_jid, conference in bookmarks.items():
|
||||
item_elt = domish.Element((pubsub.NS_PUBSUB, "item"))
|
||||
item_elt["id"] = bookmark_jid.full()
|
||||
item_elt.addChild(conference.to_element())
|
||||
bookmark_items.append(item_elt)
|
||||
|
||||
await self._p.send_items(
|
||||
client,
|
||||
None,
|
||||
NS_BOOKMARKS2,
|
||||
bookmark_items,
|
||||
extra={
|
||||
self._p.EXTRA_PUBLISH_OPTIONS: {
|
||||
"pubsub#access_model": self._p.ACCESS_WHITELIST
|
||||
},
|
||||
self._p.EXTRA_AUTOCREATE: True,
|
||||
},
|
||||
)
|
||||
else:
|
||||
log.debug(
|
||||
f"[{client.profile}] No compatibility layer found, we use legacy "
|
||||
"bookmarks."
|
||||
)
|
||||
# XXX: We add every bookmark one by one, which is inefficient. The legacy
|
||||
# plugin likely implemented this way because end-users typically add bookmarks
|
||||
# individually. Nowadays, very few servers, if any, still implement XEP-0048
|
||||
# without the XEP-0402 compatibility layer, so it's not worth spending time to
|
||||
# improve the legacy XEP-0048 plugin.
|
||||
for bookmark_jid, conference in bookmarks.items():
|
||||
bookmark_data = {}
|
||||
if conference.autojoin:
|
||||
bookmark_data["autojoin"] = C.BOOL_TRUE
|
||||
for attribute in ("name", "nick", "password"):
|
||||
value = getattr(conference, attribute)
|
||||
if value:
|
||||
bookmark_data[attribute] = value
|
||||
await self._legacy.add_bookmark(
|
||||
self._legacy.MUC_TYPE,
|
||||
bookmark_jid,
|
||||
bookmark_data,
|
||||
storage_type="private",
|
||||
profile_key=client.profile,
|
||||
)
|
||||
|
|
@ -16,27 +16,26 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
from libervia.backend.core.i18n import _
|
||||
|
||||
### logging ###
|
||||
from collections import OrderedDict
|
||||
from glob import iglob
|
||||
from importlib import import_module
|
||||
import inspect
|
||||
import logging as log
|
||||
|
||||
log.basicConfig(level=log.WARNING, format="[%(name)s] %(message)s")
|
||||
###
|
||||
|
||||
import sys
|
||||
import os
|
||||
import os.path
|
||||
import argparse
|
||||
import inspect
|
||||
import tty
|
||||
import termios
|
||||
from pathlib import Path
|
||||
from glob import iglob
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
from typing import Optional, Set, Union
|
||||
from importlib import import_module
|
||||
from libervia.frontends.tools.jid import JID
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from rich import console
|
||||
|
||||
from libervia.backend.core import exceptions
|
||||
from libervia.backend.core.i18n import _
|
||||
from libervia.backend.tools import config
|
||||
from libervia.backend.tools.common import dynamic_import
|
||||
from libervia.backend.tools.common import uri
|
||||
|
|
@ -44,15 +43,17 @@ from libervia.backend.tools.common import date_utils
|
|||
from libervia.backend.tools.common import utils
|
||||
from libervia.backend.tools.common import data_format
|
||||
from libervia.backend.tools.common.ansi import ANSI as A
|
||||
from libervia.backend.core import exceptions
|
||||
import libervia.cli
|
||||
from libervia.cli.loops import QuitException, get_libervia_cli_loop
|
||||
from libervia.cli.constants import Const as C
|
||||
from libervia.cli.loops import QuitException, get_libervia_cli_loop
|
||||
from libervia.frontends.bridge.bridge_frontend import BridgeException
|
||||
from libervia.frontends.tools import aio, misc
|
||||
import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI
|
||||
from collections import OrderedDict
|
||||
from rich import console
|
||||
from libervia.frontends.tools.jid import JID
|
||||
|
||||
log.basicConfig(level=log.WARNING, format="[%(name)s] %(message)s")
|
||||
###
|
||||
|
||||
|
||||
|
||||
## bridge handling
|
||||
# we get bridge name from conf and initialise the right class accordingly
|
||||
|
|
@ -90,6 +91,21 @@ PROGRESS_DELAY = 0.1 # the progression will be checked every PROGRESS_DELAY s
|
|||
def date_decoder(arg):
|
||||
return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL)
|
||||
|
||||
def optional_bool_decoder(arg: str) -> bool:
|
||||
"""Decode an optional string to a boolean value.
|
||||
|
||||
@param value: The input string to decode.
|
||||
@return: The decoded boolean value.
|
||||
@raise ValueError: If the input string is not a valid boolean representation.
|
||||
"""
|
||||
lower_arg = arg.lower()
|
||||
if lower_arg in ['true', '1', 't', 'y', 'yes']:
|
||||
return True
|
||||
elif lower_arg in ['false', '0', 'f', 'n', 'no']:
|
||||
return False
|
||||
else:
|
||||
raise ValueError(f"Invalid boolean value: {arg}")
|
||||
|
||||
|
||||
class LiberviaCli:
|
||||
"""
|
||||
|
|
|
|||
173
libervia/cli/bookmarks_legacy.py
Normal file
173
libervia/cli/bookmarks_legacy.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
# Libervia CLI
|
||||
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from . import base
|
||||
from libervia.backend.core.i18n import _
|
||||
from libervia.cli.constants import Const as C
|
||||
|
||||
STORAGE_LOCATIONS = ("local", "private", "pubsub")
|
||||
TYPES = ("muc", "url")
|
||||
|
||||
|
||||
class BookmarksCommon(base.CommandBase):
|
||||
"""Class used to group common options of bookmarks subcommands"""
|
||||
|
||||
def add_parser_options(self, location_default="all"):
|
||||
self.parser.add_argument(
|
||||
"-l",
|
||||
"--location",
|
||||
type=str,
|
||||
choices=(location_default,) + STORAGE_LOCATIONS,
|
||||
default=location_default,
|
||||
help=_("storage location (default: %(default)s)"),
|
||||
)
|
||||
self.parser.add_argument(
|
||||
"-t",
|
||||
"--type",
|
||||
type=str,
|
||||
choices=TYPES,
|
||||
default=TYPES[0],
|
||||
help=_("bookmarks type (default: %(default)s)"),
|
||||
)
|
||||
|
||||
|
||||
class BookmarksList(BookmarksCommon):
|
||||
def __init__(self, host):
|
||||
super(BookmarksList, self).__init__(host, "list", help=_("list bookmarks"))
|
||||
|
||||
async def start(self):
|
||||
try:
|
||||
data = await self.host.bridge.bookmarks_legacy_list(
|
||||
self.args.type, self.args.location, self.host.profile
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get bookmarks list: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
mess = []
|
||||
for location in STORAGE_LOCATIONS:
|
||||
if not data[location]:
|
||||
continue
|
||||
loc_mess = []
|
||||
loc_mess.append(f"{location}:")
|
||||
book_mess = []
|
||||
for book_link, book_data in list(data[location].items()):
|
||||
name = book_data.get("name")
|
||||
autojoin = book_data.get("autojoin", "false") == "true"
|
||||
nick = book_data.get("nick")
|
||||
book_mess.append(
|
||||
"\t%s[%s%s]%s"
|
||||
% (
|
||||
(name + " ") if name else "",
|
||||
book_link,
|
||||
" (%s)" % nick if nick else "",
|
||||
" (*)" if autojoin else "",
|
||||
)
|
||||
)
|
||||
loc_mess.append("\n".join(book_mess))
|
||||
mess.append("\n".join(loc_mess))
|
||||
|
||||
print("\n\n".join(mess))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class BookmarksRemove(BookmarksCommon):
|
||||
def __init__(self, host):
|
||||
super(BookmarksRemove, self).__init__(host, "remove", help=_("remove a bookmark"))
|
||||
|
||||
def add_parser_options(self):
|
||||
super(BookmarksRemove, self).add_parser_options()
|
||||
self.parser.add_argument(
|
||||
"bookmark", help=_("jid (for muc bookmark) or url of to remove")
|
||||
)
|
||||
self.parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
help=_("delete bookmark without confirmation"),
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
if not self.args.force:
|
||||
await self.host.confirm_or_quit(_("Are you sure to delete this bookmark?"))
|
||||
|
||||
try:
|
||||
await self.host.bridge.bookmarks_legacy_remove(
|
||||
self.args.type, self.args.bookmark, self.args.location, self.host.profile
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(_("can't delete bookmark: {e}").format(e=e), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(_("bookmark deleted"))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class BookmarksAdd(BookmarksCommon):
|
||||
def __init__(self, host):
|
||||
super(BookmarksAdd, self).__init__(host, "add", help=_("add a bookmark"))
|
||||
|
||||
def add_parser_options(self):
|
||||
super(BookmarksAdd, self).add_parser_options(location_default="auto")
|
||||
self.parser.add_argument(
|
||||
"bookmark", help=_("jid (for muc bookmark) or url of to remove")
|
||||
)
|
||||
self.parser.add_argument("-n", "--name", help=_("bookmark name"))
|
||||
muc_group = self.parser.add_argument_group(_("MUC specific options"))
|
||||
muc_group.add_argument("-N", "--nick", help=_("nickname"))
|
||||
muc_group.add_argument(
|
||||
"-a",
|
||||
"--autojoin",
|
||||
action="store_true",
|
||||
help=_("join room on profile connection"),
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
if self.args.type == "url" and (self.args.autojoin or self.args.nick is not None):
|
||||
self.parser.error(_("You can't use --autojoin or --nick with --type url"))
|
||||
data = {}
|
||||
if self.args.autojoin:
|
||||
data["autojoin"] = "true"
|
||||
if self.args.nick is not None:
|
||||
data["nick"] = self.args.nick
|
||||
if self.args.name is not None:
|
||||
data["name"] = self.args.name
|
||||
try:
|
||||
await self.host.bridge.bookmarks_legacy_add(
|
||||
self.args.type,
|
||||
self.args.bookmark,
|
||||
data,
|
||||
self.args.location,
|
||||
self.host.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't add bookmark: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(_("bookmark successfully added"))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class BookmarksLegacy(base.CommandBase):
|
||||
subcommands = (BookmarksList, BookmarksRemove, BookmarksAdd)
|
||||
|
||||
def __init__(self, host):
|
||||
super(BookmarksLegacy, self).__init__(
|
||||
host, "legacy", use_profile=False, help=_("manage legacy bookmarks")
|
||||
)
|
||||
|
|
@ -17,86 +17,70 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from . import base
|
||||
from rich.table import Table
|
||||
|
||||
from libervia.backend.core.i18n import _
|
||||
from libervia.backend.tools.common import data_format
|
||||
from libervia.cli.constants import Const as C
|
||||
|
||||
from . import base
|
||||
from .bookmarks_legacy import BookmarksLegacy
|
||||
|
||||
__commands__ = ["Bookmarks"]
|
||||
|
||||
STORAGE_LOCATIONS = ("local", "private", "pubsub")
|
||||
TYPES = ("muc", "url")
|
||||
|
||||
|
||||
class BookmarksCommon(base.CommandBase):
|
||||
"""Class used to group common options of bookmarks subcommands"""
|
||||
|
||||
def add_parser_options(self, location_default="all"):
|
||||
self.parser.add_argument(
|
||||
"-l",
|
||||
"--location",
|
||||
type=str,
|
||||
choices=(location_default,) + STORAGE_LOCATIONS,
|
||||
default=location_default,
|
||||
help=_("storage location (default: %(default)s)"),
|
||||
)
|
||||
self.parser.add_argument(
|
||||
"-t",
|
||||
"--type",
|
||||
type=str,
|
||||
choices=TYPES,
|
||||
default=TYPES[0],
|
||||
help=_("bookmarks type (default: %(default)s)"),
|
||||
)
|
||||
|
||||
|
||||
class BookmarksList(BookmarksCommon):
|
||||
class BookmarksList(base.CommandBase):
|
||||
def __init__(self, host):
|
||||
super(BookmarksList, self).__init__(host, "list", help=_("list bookmarks"))
|
||||
extra_outputs = {"default": self.default_output}
|
||||
super().__init__(
|
||||
host, "list", help=_("list bookmarks"),
|
||||
use_output=C.OUTPUT_COMPLEX,
|
||||
extra_outputs=extra_outputs
|
||||
)
|
||||
|
||||
def add_parser_options(self):
|
||||
pass
|
||||
|
||||
def default_output(self, data: dict) -> None:
|
||||
table = Table(title="📚 " + _("Group Chat Bookmarks"))
|
||||
table.add_column("🌐 JID")
|
||||
table.add_column("📝 " + _("Name"))
|
||||
table.add_column("👤 " + _("Nick"))
|
||||
table.add_column("🔒 " + _("Password"))
|
||||
table.add_column("🚪 " + _("Joined"))
|
||||
|
||||
for jid, conference_data in data.items():
|
||||
table.add_row(
|
||||
str(jid),
|
||||
conference_data.get("name", ""),
|
||||
conference_data.get("nick", ""),
|
||||
conference_data.get("password", ""),
|
||||
"✅" if conference_data.get("autojoin", False) else "❌"
|
||||
)
|
||||
|
||||
self.console.print(table)
|
||||
|
||||
async def start(self):
|
||||
try:
|
||||
data = await self.host.bridge.bookmarks_list(
|
||||
self.args.type, self.args.location, self.host.profile
|
||||
)
|
||||
data = data_format.deserialise(await self.host.bridge.bookmarks_list(
|
||||
"", self.host.profile
|
||||
))
|
||||
except Exception as e:
|
||||
self.disp(f"can't get bookmarks list: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
return
|
||||
|
||||
mess = []
|
||||
for location in STORAGE_LOCATIONS:
|
||||
if not data[location]:
|
||||
continue
|
||||
loc_mess = []
|
||||
loc_mess.append(f"{location}:")
|
||||
book_mess = []
|
||||
for book_link, book_data in list(data[location].items()):
|
||||
name = book_data.get("name")
|
||||
autojoin = book_data.get("autojoin", "false") == "true"
|
||||
nick = book_data.get("nick")
|
||||
book_mess.append(
|
||||
"\t%s[%s%s]%s"
|
||||
% (
|
||||
(name + " ") if name else "",
|
||||
book_link,
|
||||
" (%s)" % nick if nick else "",
|
||||
" (*)" if autojoin else "",
|
||||
)
|
||||
)
|
||||
loc_mess.append("\n".join(book_mess))
|
||||
mess.append("\n".join(loc_mess))
|
||||
|
||||
print("\n\n".join(mess))
|
||||
await self.output(data)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class BookmarksRemove(BookmarksCommon):
|
||||
class BookmarksRemove(base.CommandBase):
|
||||
def __init__(self, host):
|
||||
super(BookmarksRemove, self).__init__(host, "remove", help=_("remove a bookmark"))
|
||||
super().__init__(host, "remove", help=_("remove a bookmark"))
|
||||
|
||||
def add_parser_options(self):
|
||||
super(BookmarksRemove, self).add_parser_options()
|
||||
self.parser.add_argument(
|
||||
"bookmark", help=_("jid (for muc bookmark) or url of to remove")
|
||||
"bookmark", help=_("jid of the bookmark to remove")
|
||||
)
|
||||
self.parser.add_argument(
|
||||
"-f",
|
||||
|
|
@ -107,11 +91,14 @@ class BookmarksRemove(BookmarksCommon):
|
|||
|
||||
async def start(self):
|
||||
if not self.args.force:
|
||||
await self.host.confirm_or_quit(_("Are you sure to delete this bookmark?"))
|
||||
await self.host.confirm_or_quit(
|
||||
_("Are you sure to delete the bookmark {bookmark_id!r}?")
|
||||
.format(bookmark_id=self.args.bookmark)
|
||||
)
|
||||
|
||||
try:
|
||||
await self.host.bridge.bookmarks_remove(
|
||||
self.args.type, self.args.bookmark, self.args.location, self.host.profile
|
||||
await self.host.bridge.bookmark_remove(
|
||||
self.args.bookmark, self.host.profile
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(_("can't delete bookmark: {e}").format(e=e), error=True)
|
||||
|
|
@ -121,55 +108,88 @@ class BookmarksRemove(BookmarksCommon):
|
|||
self.host.quit()
|
||||
|
||||
|
||||
class BookmarksAdd(BookmarksCommon):
|
||||
class BookmarksSet(base.CommandBase):
|
||||
def __init__(self, host):
|
||||
super(BookmarksAdd, self).__init__(host, "add", help=_("add a bookmark"))
|
||||
super().__init__(
|
||||
host, "set", help=_("add or update a bookmark")
|
||||
)
|
||||
|
||||
def add_parser_options(self):
|
||||
super(BookmarksAdd, self).add_parser_options(location_default="auto")
|
||||
self.parser.add_argument("bookmark", help=_("jid of the chat room"))
|
||||
self.parser.add_argument("-n", "--name", help=_("bookmark name"), dest="name")
|
||||
self.parser.add_argument(
|
||||
"bookmark", help=_("jid (for muc bookmark) or url of to remove")
|
||||
"-j",
|
||||
"--join",
|
||||
nargs="?",
|
||||
# Value use when option is missing.
|
||||
default=None,
|
||||
# Value use when option is used, but value is not specified.
|
||||
const=True,
|
||||
type=base.optional_bool_decoder,
|
||||
# The bookmark attribute is called "autojoin" for historical reason, but it's
|
||||
# now used a "join" flag, so we use ``join`` here for the option.
|
||||
dest="autojoin",
|
||||
metavar="BOOL",
|
||||
help=_("join the conference room"),
|
||||
)
|
||||
self.parser.add_argument("-n", "--name", help=_("bookmark name"))
|
||||
muc_group = self.parser.add_argument_group(_("MUC specific options"))
|
||||
muc_group.add_argument("-N", "--nick", help=_("nickname"))
|
||||
muc_group.add_argument(
|
||||
"-a",
|
||||
"--autojoin",
|
||||
self.parser.add_argument(
|
||||
"-N",
|
||||
"--nick", help=_("preferred roomnick for the chatroom")
|
||||
)
|
||||
self.parser.add_argument(
|
||||
"-P",
|
||||
"--password", help=_("password used to access the chatroom")
|
||||
)
|
||||
self.parser.add_argument(
|
||||
"-u",
|
||||
"--update",
|
||||
action="store_true",
|
||||
help=_("join room on profile connection"),
|
||||
help=_("update bookmark data instead of replacing")
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
if self.args.type == "url" and (self.args.autojoin or self.args.nick is not None):
|
||||
self.parser.error(_("You can't use --autojoin or --nick with --type url"))
|
||||
data = {}
|
||||
if self.args.autojoin:
|
||||
data["autojoin"] = "true"
|
||||
if self.args.nick is not None:
|
||||
data["nick"] = self.args.nick
|
||||
if self.args.name is not None:
|
||||
data["name"] = self.args.name
|
||||
conference_data = {
|
||||
"autojoin": self.args.autojoin,
|
||||
"name": self.args.name,
|
||||
"nick": self.args.nick,
|
||||
"password": self.args.password,
|
||||
}
|
||||
|
||||
conference_data = {k: v for k, v in conference_data.items() if v is not None}
|
||||
if self.args.update:
|
||||
try:
|
||||
old_conference_data = data_format.deserialise(
|
||||
await self.host.bridge.bookmark_get(
|
||||
self.args.bookmark, self.host.profile
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(
|
||||
f"Can't find existing bookmark {self.args.bookmark!r}: {e}. We "
|
||||
"create it.",
|
||||
error=True
|
||||
)
|
||||
else:
|
||||
old_conference_data.update(conference_data)
|
||||
conference_data = old_conference_data
|
||||
|
||||
try:
|
||||
await self.host.bridge.bookmarks_add(
|
||||
self.args.type,
|
||||
self.args.bookmark,
|
||||
data,
|
||||
self.args.location,
|
||||
await self.host.bridge.bookmarks_set(
|
||||
data_format.serialise({self.args.bookmark: conference_data}),
|
||||
self.host.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't add bookmark: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(_("bookmark successfully added"))
|
||||
self.disp(_("bookmark successfully set"))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Bookmarks(base.CommandBase):
|
||||
subcommands = (BookmarksList, BookmarksRemove, BookmarksAdd)
|
||||
subcommands = (BookmarksList, BookmarksSet, BookmarksRemove, BookmarksLegacy)
|
||||
|
||||
def __init__(self, host):
|
||||
super(Bookmarks, self).__init__(
|
||||
super().__init__(
|
||||
host, "bookmarks", use_profile=False, help=_("manage bookmarks")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -154,16 +154,14 @@ class Unregister(base.CommandBase):
|
|||
async def start(self):
|
||||
if not self.args.force:
|
||||
await self.host.confirm_or_quit(
|
||||
_(
|
||||
"Are you sure that you want to unregister from {gateway_jid}?"
|
||||
).format(gateway_jid=self.args.gateway_jid),
|
||||
_("Gateway unregistration cancelled.")
|
||||
_("Are you sure that you want to unregister from {gateway_jid}?").format(
|
||||
gateway_jid=self.args.gateway_jid
|
||||
),
|
||||
_("Gateway unregistration cancelled."),
|
||||
)
|
||||
|
||||
try:
|
||||
await self.host.bridge.in_band_unregister(
|
||||
self.args.gateway_jid, self.profile
|
||||
)
|
||||
await self.host.bridge.in_band_unregister(self.args.gateway_jid, self.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't unregister from gateway: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ class Send(base.CommandBase):
|
|||
addressing_group = self.parser.add_argument_group(
|
||||
"addressing commands",
|
||||
description="Commands to add addressing metadata, and/or to send message to "
|
||||
"multiple recipients."
|
||||
"multiple recipients.",
|
||||
)
|
||||
for arg_name in RECIPIENTS_ARGS:
|
||||
addressing_group.add_argument(
|
||||
|
|
@ -125,7 +125,7 @@ class Send(base.CommandBase):
|
|||
action="append",
|
||||
metavar=("JID", "DESCRIPTION"),
|
||||
help=f'extra "{arg_name.upper()}" recipient(s), may be used several '
|
||||
'times',
|
||||
"times",
|
||||
)
|
||||
for arg_name in REPLY_ARGS:
|
||||
addressing_group.add_argument(
|
||||
|
|
@ -133,13 +133,12 @@ class Send(base.CommandBase):
|
|||
nargs="+",
|
||||
action="append",
|
||||
metavar=("JID", "DESCRIPTION"),
|
||||
help=f'ask to reply to this JID, may be used several '
|
||||
'times',
|
||||
help=f"ask to reply to this JID, may be used several times",
|
||||
)
|
||||
addressing_group.add_argument(
|
||||
"--no-reply",
|
||||
action="store_true",
|
||||
help="flag this message as not requiring replies"
|
||||
help="flag this message as not requiring replies",
|
||||
)
|
||||
syntax = self.parser.add_mutually_exclusive_group()
|
||||
syntax.add_argument("-x", "--xhtml", action="store_true", help=_("XHTML body"))
|
||||
|
|
|
|||
Loading…
Reference in a new issue