# 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 . # XEP-0048: Bookmarks import logging import copy from collections import OrderedDict import nbxmpp from gajim.common import app from gajim.common import helpers from gajim.common.const import BookmarkStorageType from gajim.common.nec import NetworkIncomingEvent from gajim.common.modules.util import from_xs_boolean from gajim.common.modules.util import to_xs_boolean log = logging.getLogger('gajim.c.m.bookmarks') NS_GAJIM_BM = 'xmpp:gajim.org/bookmarks' class Bookmarks: def __init__(self, con): self._con = con self._account = con.name self.bookmarks = {} self.available = False self.handlers = [] def get_sorted_bookmarks(self, short_name=False): # This returns a sorted by name copy of the bookmarks sorted_bookmarks = {} for jid, bookmarks in self.bookmarks.items(): bookmark_copy = copy.deepcopy(bookmarks) if not bookmark_copy['name']: # No name was given for this bookmark # Use the first part of JID instead name = jid.split("@")[0] bookmark_copy['name'] = name if short_name: name = bookmark_copy['name'] name = (name[:42] + '..') if len(name) > 42 else name bookmark_copy['name'] = name sorted_bookmarks[jid] = bookmark_copy return OrderedDict( sorted(sorted_bookmarks.items(), key=lambda bookmark: bookmark[1]['name'].lower())) def _pubsub_support(self) -> bool: return (self._con.get_module('PEP').supported and self._con.get_module('PubSub').publish_options) def get_bookmarks(self, storage_type=None): if not app.account_is_connected(self._account): return if storage_type in (None, BookmarkStorageType.PUBSUB): if self._pubsub_support(): self._request_pubsub_bookmarks() else: # Fallback, request private storage self._request_private_bookmarks() else: log.info('Request Bookmarks (PrivateStorage)') self._request_private_bookmarks() def _request_pubsub_bookmarks(self) -> None: log.info('Request Bookmarks (PubSub)') self._con.get_module('PubSub').send_pb_retrieve( '', 'storage:bookmarks', cb=self._pubsub_bookmarks_received) def _pubsub_bookmarks_received(self, _con, stanza): if not nbxmpp.isResultNode(stanza): log.info('No pubsub bookmarks: %s', stanza.getError()) # Fallback, request private storage self._request_private_bookmarks() return self.available = True log.info('Received Bookmarks (PubSub)') self._parse_bookmarks(stanza) self._request_private_bookmarks() def _request_private_bookmarks(self) -> None: if not app.account_is_connected(self._account): return iq = nbxmpp.Iq(typ='get') query = iq.addChild(name='query', namespace=nbxmpp.NS_PRIVATE) query.addChild(name='storage', namespace='storage:bookmarks') log.info('Request Bookmarks (PrivateStorage)') self._con.connection.SendAndCallForResponse( iq, self._private_bookmarks_received) def _private_bookmarks_received(self, stanza: nbxmpp.Iq) -> None: if not nbxmpp.isResultNode(stanza): log.info('No private bookmarks: %s', stanza.getError()) else: self.available = True log.info('Received Bookmarks (PrivateStorage)') merged = self._parse_bookmarks(stanza, check_merge=True) if merged and self._pubsub_support(): log.info('Merge PrivateStorage with PubSub') self.store_bookmarks(BookmarkStorageType.PUBSUB) self.auto_join_bookmarks() app.nec.push_incoming_event(BookmarksReceivedEvent( None, account=self._account)) @staticmethod def _get_storage_node(stanza): node = stanza.getTag('pubsub', namespace=nbxmpp.NS_PUBSUB) if node is None: node = stanza.getTag('event', namespace=nbxmpp.NS_PUBSUB_EVENT) if node is None: # Private Storage query = stanza.getQuery() if query is None: return storage = query.getTag('storage', namespace=nbxmpp.NS_BOOKMARKS) if storage is None: return return storage items_node = node.getTag('items') if items_node is None: return if items_node.getAttr('node') != nbxmpp.NS_BOOKMARKS: return item_node = items_node.getTag('item') if item_node is None: return storage = item_node.getTag('storage', namespace=nbxmpp.NS_BOOKMARKS) if storage is None: return return storage def _parse_bookmarks(self, stanza: nbxmpp.Iq, check_merge: bool = False) -> bool: merged = False storage = self._get_storage_node(stanza) if storage is None: return False confs = storage.getTags('conference') for conf in confs: autojoin_val = conf.getAttr('autojoin') if not autojoin_val: # not there (it's optional) autojoin_val = False minimize_val = conf.getTag('minimize', namespace=NS_GAJIM_BM) if not minimize_val: # not there, try old Gajim behaviour minimize_val = conf.getAttr('minimize') if not minimize_val: # not there (it's optional) minimize_val = False else: minimize_val = minimize_val.getData() print_status = conf.getTag('print_status', namespace=NS_GAJIM_BM) if not print_status: # not there, try old Gajim behaviour print_status = conf.getTagData('print_status') if not print_status: # not there, try old Gajim behaviour print_status = conf.getTagData('show_status') else: print_status = print_status.getData() try: jid = helpers.parse_jid(conf.getAttr('jid')) except helpers.InvalidFormat: log.warning('Invalid JID: %s, ignoring it', conf.getAttr('jid')) continue bookmark = { 'name': conf.getAttr('name'), 'password': conf.getTagData('password'), 'nick': conf.getTagData('nick'), 'print_status': print_status } try: bookmark['autojoin'] = from_xs_boolean(autojoin_val) bookmark['minimize'] = from_xs_boolean(minimize_val) except ValueError as error: log.warning(error) continue if check_merge: if jid in self.bookmarks: continue merged = True log.debug('Found Bookmark: %s', jid) self.bookmarks[jid] = bookmark return merged def _build_storage_node(self) -> nbxmpp.Node: storage_node = nbxmpp.Node( tag='storage', attrs={'xmlns': 'storage:bookmarks'}) for jid, bm in self.bookmarks.items(): conf_node = storage_node.addChild(name="conference") conf_node.setAttr('jid', jid) conf_node.setAttr('autojoin', to_xs_boolean(bm['autojoin'])) conf_node.setAttr('name', bm['name']) conf_node.setTag('minimize', namespace=NS_GAJIM_BM).setData( to_xs_boolean(bm['minimize'])) # Only add optional elements if not empty # Note: need to handle both None and '' as empty # thus shouldn't use "is not None" if bm.get('nick', None): conf_node.setTagData('nick', bm['nick']) if bm.get('password', None): conf_node.setTagData('password', bm['password']) if bm.get('print_status', None): conf_node.setTag( 'print_status', namespace=NS_GAJIM_BM).setData(bm['print_status']) return storage_node @staticmethod def get_bookmark_publish_options() -> nbxmpp.Node: options = nbxmpp.Node(nbxmpp.NS_DATA + ' x', attrs={'type': 'submit'}) field = options.addChild('field', attrs={'var': 'FORM_TYPE', 'type': 'hidden'}) field.setTagData('value', nbxmpp.NS_PUBSUB_PUBLISH_OPTIONS) field = options.addChild('field', attrs={'var': 'pubsub#access_model'}) field.setTagData('value', 'whitelist') return options def store_bookmarks(self, storage_type=None): if not app.account_is_connected(self._account): return storage_node = self._build_storage_node() if storage_type is None: if self._pubsub_support(): self._pubsub_store(storage_node) self._private_store(storage_node) elif storage_type == BookmarkStorageType.PUBSUB: if self._pubsub_support(): self._pubsub_store(storage_node) elif storage_type == BookmarkStorageType.PRIVATE: self._private_store(storage_node) def _pubsub_store(self, storage_node: nbxmpp.Node) -> None: self._con.get_module('PubSub').send_pb_publish( '', 'storage:bookmarks', storage_node, 'current', options=self.get_bookmark_publish_options(), cb=self._pubsub_store_result) log.info('Publish Bookmarks (PubSub)') def _private_store(self, storage_node: nbxmpp.Node) -> None: iq = nbxmpp.Iq('set', nbxmpp.NS_PRIVATE, payload=storage_node) log.info('Publish Bookmarks (PrivateStorage)') self._con.connection.SendAndCallForResponse( iq, self._private_store_result) @staticmethod def _pubsub_store_result(_con, stanza): if not nbxmpp.isResultNode(stanza): log.error('Error: %s', stanza.getError()) return @staticmethod def _private_store_result(stanza: nbxmpp.Iq) -> None: if not nbxmpp.isResultNode(stanza): log.error('Error: %s', stanza.getError()) return def auto_join_bookmarks(self) -> None: if app.is_invisible(self._account): return for jid, bm in self.bookmarks.items(): if bm['autojoin']: # Only join non-opened groupchats. Opened one are already # auto-joined on re-connection if jid not in app.gc_connected[self._account]: # we are not already connected app.interface.join_gc_room( self._account, jid, bm['nick'], bm['password'], minimize=bm['minimize']) def add_bookmark(self, name, jid, autojoin, minimize, password, nick): self.bookmarks[jid] = { 'name': name, 'autojoin': autojoin, 'minimize': minimize, 'password': password, 'nick': nick, 'print_status': None} self.store_bookmarks() app.nec.push_incoming_event(BookmarksReceivedEvent( None, account=self._account)) def get_name_from_bookmark(self, jid: str) -> str: fallback = jid.split('@')[0] try: return self.bookmarks[jid]['name'] or fallback except KeyError: return fallback def purge_pubsub_bookmarks(self) -> None: log.info('Purge/Delete Bookmarks on PubSub, ' 'because publish options are not available') self._con.get_module('PubSub').send_pb_purge('', 'storage:bookmarks') self._con.get_module('PubSub').send_pb_delete('', 'storage:bookmarks') class BookmarksReceivedEvent(NetworkIncomingEvent): name = 'bookmarks-received' def get_instance(*args, **kwargs): return Bookmarks(*args, **kwargs), 'Bookmarks'