354 lines
12 KiB
Python
354 lines
12 KiB
Python
|
# This file is part of Gajim.
|
||
|
#
|
||
|
# Gajim is free software; you can redistribute it and/or modify
|
||
|
# it under the terms of the GNU General Public License as published
|
||
|
# by the Free Software Foundation; version 3 only.
|
||
|
#
|
||
|
# Gajim is distributed in the hope that it will be useful,
|
||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
# GNU General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU General Public License
|
||
|
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
||
|
# XEP-0048: Bookmarks
|
||
|
|
||
|
from typing import Any
|
||
|
from typing import List
|
||
|
from typing import Dict
|
||
|
from typing import Set
|
||
|
from typing import Tuple
|
||
|
from typing import Union
|
||
|
from typing import Optional
|
||
|
|
||
|
import functools
|
||
|
|
||
|
from nbxmpp.namespaces import Namespace
|
||
|
from nbxmpp.protocol import JID
|
||
|
from nbxmpp.structs import BookmarkData
|
||
|
from gi.repository import GLib
|
||
|
|
||
|
from gajim.common import app
|
||
|
from gajim.common.nec import NetworkEvent
|
||
|
from gajim.common.modules.base import BaseModule
|
||
|
from gajim.common.modules.util import event_node
|
||
|
|
||
|
|
||
|
NODE_MAX_NS = 'http://jabber.org/protocol/pubsub#config-node-max'
|
||
|
|
||
|
|
||
|
class Bookmarks(BaseModule):
|
||
|
def __init__(self, con):
|
||
|
BaseModule.__init__(self, con)
|
||
|
self._register_pubsub_handler(self._bookmark_event_received)
|
||
|
self._register_pubsub_handler(self._bookmark_1_event_received)
|
||
|
self._conversion = False
|
||
|
self._compat = False
|
||
|
self._compat_pep = False
|
||
|
self._node_max = False
|
||
|
self._bookmarks = {}
|
||
|
self._join_timeouts = []
|
||
|
self._request_in_progress = True
|
||
|
|
||
|
@property
|
||
|
def conversion(self) -> bool:
|
||
|
return self._conversion
|
||
|
|
||
|
@property
|
||
|
def compat(self) -> bool:
|
||
|
return self._compat
|
||
|
|
||
|
@property
|
||
|
def compat_pep(self) -> bool:
|
||
|
return self._compat_pep
|
||
|
|
||
|
@property
|
||
|
def bookmarks(self) -> List[BookmarkData]:
|
||
|
return self._bookmarks.values()
|
||
|
|
||
|
@property
|
||
|
def pep_bookmarks_used(self) -> bool:
|
||
|
return self._bookmark_module() == 'PEPBookmarks'
|
||
|
|
||
|
@property
|
||
|
def nativ_bookmarks_used(self) -> bool:
|
||
|
return self._bookmark_module() == 'NativeBookmarks'
|
||
|
|
||
|
@event_node(Namespace.BOOKMARKS)
|
||
|
def _bookmark_event_received(self, _con, _stanza, properties):
|
||
|
if properties.pubsub_event.retracted:
|
||
|
return
|
||
|
|
||
|
if not properties.is_self_message:
|
||
|
self._log.warning('%s has an open access bookmarks node',
|
||
|
properties.jid)
|
||
|
return
|
||
|
|
||
|
if not self.pep_bookmarks_used:
|
||
|
return
|
||
|
|
||
|
if self._request_in_progress:
|
||
|
self._log.info('Ignore update, pubsub request in progress')
|
||
|
return
|
||
|
|
||
|
bookmarks = self._convert_to_dict(properties.pubsub_event.data)
|
||
|
|
||
|
old_bookmarks = self._bookmarks.copy()
|
||
|
self._bookmarks = bookmarks
|
||
|
self._act_on_changed_bookmarks(old_bookmarks)
|
||
|
app.nec.push_incoming_event(
|
||
|
NetworkEvent('bookmarks-received', account=self._account))
|
||
|
|
||
|
@event_node(Namespace.BOOKMARKS_1)
|
||
|
def _bookmark_1_event_received(self, _con, _stanza, properties):
|
||
|
if not properties.is_self_message:
|
||
|
self._log.warning('%s has an open access bookmarks node',
|
||
|
properties.jid)
|
||
|
return
|
||
|
|
||
|
if not self.nativ_bookmarks_used:
|
||
|
return
|
||
|
|
||
|
if self._request_in_progress:
|
||
|
self._log.info('Ignore update, pubsub request in progress')
|
||
|
return
|
||
|
|
||
|
old_bookmarks = self._bookmarks.copy()
|
||
|
|
||
|
if properties.pubsub_event.deleted or properties.pubsub_event.purged:
|
||
|
self._log.info('Bookmark node deleted/purged')
|
||
|
self._bookmarks = {}
|
||
|
|
||
|
elif properties.pubsub_event.retracted:
|
||
|
jid = properties.pubsub_event.id
|
||
|
self._log.info('Retract: %s', jid)
|
||
|
bookmark = self._bookmarks.get(jid)
|
||
|
if bookmark is not None:
|
||
|
self._bookmarks.pop(bookmark, None)
|
||
|
|
||
|
else:
|
||
|
new_bookmark = properties.pubsub_event.data
|
||
|
self._bookmarks[new_bookmark.jid] = properties.pubsub_event.data
|
||
|
|
||
|
self._act_on_changed_bookmarks(old_bookmarks)
|
||
|
app.nec.push_incoming_event(
|
||
|
NetworkEvent('bookmarks-received', account=self._account))
|
||
|
|
||
|
def pass_disco(self, info):
|
||
|
self._node_max = NODE_MAX_NS in info.features
|
||
|
self._compat_pep = Namespace.BOOKMARKS_COMPAT_PEP in info.features
|
||
|
self._compat = Namespace.BOOKMARKS_COMPAT in info.features
|
||
|
self._conversion = Namespace.BOOKMARK_CONVERSION in info.features
|
||
|
|
||
|
@functools.lru_cache(maxsize=1)
|
||
|
def _bookmark_module(self):
|
||
|
if not self._con.get_module('PubSub').publish_options:
|
||
|
return 'PrivateBookmarks'
|
||
|
|
||
|
if app.settings.get('dev_force_bookmark_2'):
|
||
|
return 'NativeBookmarks'
|
||
|
|
||
|
if self._compat_pep and self._node_max:
|
||
|
return 'NativeBookmarks'
|
||
|
|
||
|
if self._conversion:
|
||
|
return 'PEPBookmarks'
|
||
|
return 'PrivateBookmarks'
|
||
|
|
||
|
def _act_on_changed_bookmarks(
|
||
|
self, old_bookmarks: Dict[str, BookmarkData]) -> None:
|
||
|
|
||
|
new_bookmarks = self._convert_to_set(self._bookmarks)
|
||
|
old_bookmarks = self._convert_to_set(old_bookmarks)
|
||
|
changed = new_bookmarks - old_bookmarks
|
||
|
if not changed:
|
||
|
return
|
||
|
|
||
|
join = [jid for jid, autojoin in changed if autojoin]
|
||
|
bookmarks = []
|
||
|
for jid in join:
|
||
|
self._log.info('Schedule autojoin in 10s for: %s', jid)
|
||
|
bookmarks.append(self._bookmarks.get(jid))
|
||
|
# If another client creates a MUC, the MUC is locked until the
|
||
|
# configuration is finished. Give the user some time to finish
|
||
|
# the configuration.
|
||
|
timeout_id = GLib.timeout_add_seconds(
|
||
|
10, self._join_with_timeout, bookmarks)
|
||
|
self._join_timeouts.append(timeout_id)
|
||
|
|
||
|
# TODO: leave mucs
|
||
|
# leave = [jid for jid, autojoin in changed if not autojoin]
|
||
|
|
||
|
@staticmethod
|
||
|
def _convert_to_set(
|
||
|
bookmarks: Dict[str, BookmarkData]) -> Set[Tuple[str, bool]]:
|
||
|
|
||
|
set_ = set()
|
||
|
for jid, bookmark in bookmarks.items():
|
||
|
set_.add((jid, bookmark.autojoin))
|
||
|
return set_
|
||
|
|
||
|
@staticmethod
|
||
|
def _convert_to_dict(bookmarks: List) -> Dict[str, BookmarkData]:
|
||
|
_dict = {} # type: Dict[str, BookmarkData]
|
||
|
if bookmarks is None:
|
||
|
return _dict
|
||
|
|
||
|
for bookmark in bookmarks:
|
||
|
_dict[bookmark.jid] = bookmark
|
||
|
return _dict
|
||
|
|
||
|
def get_bookmark(self, jid: Union[str, JID]) -> BookmarkData:
|
||
|
return self._bookmarks.get(jid)
|
||
|
|
||
|
def request_bookmarks(self) -> None:
|
||
|
if not app.account_is_available(self._account):
|
||
|
return
|
||
|
|
||
|
self._request_in_progress = True
|
||
|
self._nbxmpp(self._bookmark_module()).request_bookmarks(
|
||
|
callback=self._bookmarks_received)
|
||
|
|
||
|
def _bookmarks_received(self, task: Any) -> None:
|
||
|
try:
|
||
|
bookmarks = task.finish()
|
||
|
except Exception as error:
|
||
|
self._log.warning(error)
|
||
|
bookmarks = None
|
||
|
|
||
|
self._request_in_progress = False
|
||
|
self._bookmarks = self._convert_to_dict(bookmarks)
|
||
|
self.auto_join_bookmarks()
|
||
|
app.nec.push_incoming_event(
|
||
|
NetworkEvent('bookmarks-received', account=self._account))
|
||
|
|
||
|
def store_difference(self, bookmarks: List) -> None:
|
||
|
if self.nativ_bookmarks_used:
|
||
|
retract, add_or_modify = self._determine_changed_bookmarks(
|
||
|
bookmarks, self._bookmarks)
|
||
|
|
||
|
for bookmark in retract:
|
||
|
self.remove(bookmark.jid)
|
||
|
|
||
|
if add_or_modify:
|
||
|
self.store_bookmarks(add_or_modify)
|
||
|
self._bookmarks = self._convert_to_dict(bookmarks)
|
||
|
|
||
|
else:
|
||
|
self._bookmarks = self._convert_to_dict(bookmarks)
|
||
|
self.store_bookmarks()
|
||
|
|
||
|
def store_bookmarks(self, bookmarks: list = None) -> None:
|
||
|
if not app.account_is_available(self._account):
|
||
|
return
|
||
|
|
||
|
if bookmarks is None or not self.nativ_bookmarks_used:
|
||
|
bookmarks = self._bookmarks.values()
|
||
|
|
||
|
self._nbxmpp(self._bookmark_module()).store_bookmarks(bookmarks)
|
||
|
|
||
|
app.nec.push_incoming_event(
|
||
|
NetworkEvent('bookmarks-received', account=self._account))
|
||
|
|
||
|
def _join_with_timeout(self, bookmarks: List[Any]) -> None:
|
||
|
self._join_timeouts.pop(0)
|
||
|
self.auto_join_bookmarks(bookmarks)
|
||
|
|
||
|
def auto_join_bookmarks(self,
|
||
|
bookmarks: Optional[List[Any]] = None) -> None:
|
||
|
if bookmarks is None:
|
||
|
bookmarks = self._bookmarks.values()
|
||
|
|
||
|
for bookmark in bookmarks:
|
||
|
if bookmark.autojoin:
|
||
|
# Only join non-opened groupchats. Opened one are already
|
||
|
# auto-joined on re-connection
|
||
|
if bookmark.jid not in app.gc_connected[self._account]:
|
||
|
# we are not already connected
|
||
|
self._log.info('Autojoin Bookmark: %s', bookmark.jid)
|
||
|
minimize = app.settings.get_group_chat_setting(
|
||
|
self._account,
|
||
|
bookmark.jid,
|
||
|
'minimize_on_autojoin')
|
||
|
app.interface.join_groupchat(self._account,
|
||
|
str(bookmark.jid),
|
||
|
minimized=minimize)
|
||
|
|
||
|
def modify(self, jid: str, **kwargs: Dict[str, str]) -> None:
|
||
|
bookmark = self._bookmarks.get(jid)
|
||
|
if bookmark is None:
|
||
|
return
|
||
|
|
||
|
new_bookmark = bookmark._replace(**kwargs)
|
||
|
if new_bookmark == bookmark:
|
||
|
# No change happened
|
||
|
return
|
||
|
self._log.info('Modify bookmark: %s %s', jid, kwargs)
|
||
|
self._bookmarks[jid] = new_bookmark
|
||
|
|
||
|
self.store_bookmarks([new_bookmark])
|
||
|
|
||
|
def add_or_modify(self, jid: str, **kwargs: Dict[str, str]) -> None:
|
||
|
bookmark = self._bookmarks.get(jid)
|
||
|
if bookmark is not None:
|
||
|
self.modify(jid, **kwargs)
|
||
|
return
|
||
|
|
||
|
new_bookmark = BookmarkData(jid=jid, **kwargs)
|
||
|
self._bookmarks[jid] = new_bookmark
|
||
|
self._log.info('Add new bookmark: %s', new_bookmark)
|
||
|
|
||
|
self.store_bookmarks([new_bookmark])
|
||
|
|
||
|
def remove(self, jid: JID, publish: bool = True) -> None:
|
||
|
removed = self._bookmarks.pop(jid, False)
|
||
|
if not removed:
|
||
|
return
|
||
|
if publish:
|
||
|
if self.nativ_bookmarks_used:
|
||
|
self._nbxmpp('NativeBookmarks').retract_bookmark(str(jid))
|
||
|
else:
|
||
|
self.store_bookmarks()
|
||
|
|
||
|
@staticmethod
|
||
|
def _determine_changed_bookmarks(
|
||
|
new_bookmarks: List[BookmarkData],
|
||
|
old_bookmarks: Dict[str, BookmarkData]) -> Tuple[
|
||
|
List[BookmarkData], List[BookmarkData]]:
|
||
|
|
||
|
new_jids = [bookmark.jid for bookmark in new_bookmarks]
|
||
|
new_bookmarks = set(new_bookmarks)
|
||
|
old_bookmarks = set(old_bookmarks.values())
|
||
|
|
||
|
retract = []
|
||
|
add_or_modify = []
|
||
|
changed_bookmarks = new_bookmarks.symmetric_difference(old_bookmarks)
|
||
|
|
||
|
for bookmark in changed_bookmarks:
|
||
|
if bookmark.jid not in new_jids:
|
||
|
retract.append(bookmark)
|
||
|
if bookmark in new_bookmarks:
|
||
|
add_or_modify.append(bookmark)
|
||
|
return retract, add_or_modify
|
||
|
|
||
|
def get_name_from_bookmark(self, jid: str) -> str:
|
||
|
bookmark = self._bookmarks.get(jid)
|
||
|
if bookmark is None:
|
||
|
return ''
|
||
|
return bookmark.name
|
||
|
|
||
|
def is_bookmark(self, jid: str) -> bool:
|
||
|
return jid in self._bookmarks
|
||
|
|
||
|
def _remove_timeouts(self):
|
||
|
for _id in self._join_timeouts:
|
||
|
GLib.source_remove(_id)
|
||
|
|
||
|
def cleanup(self):
|
||
|
self._remove_timeouts()
|
||
|
|
||
|
|
||
|
def get_instance(*args, **kwargs):
|
||
|
return Bookmarks(*args, **kwargs), 'Bookmarks'
|