diff --git a/gajim/app_actions.py b/gajim/app_actions.py index 759012507..8ca7a9884 100644 --- a/gajim/app_actions.py +++ b/gajim/app_actions.py @@ -44,6 +44,7 @@ from gajim.gtk.history import HistoryWindow from gajim.gtk.accounts import AccountsWindow from gajim.gtk.proxies import ManageProxies from gajim.gtk.discovery import ServiceDiscoveryWindow +from gajim.gtk.blocking import BlockingList # General Actions @@ -211,6 +212,15 @@ def on_mam_preferences(action, param): window.present() +def on_blocking_list(action, param): + account = param.get_string() + window = app.get_app_window(MamPreferences, account) + if window is None: + BlockingList(account) + else: + window.present() + + def on_history_sync(action, param): account = param.get_string() if 'history_sync' in interface.instances[account]: diff --git a/gajim/application.py b/gajim/application.py index fb206529a..b2da1f9fd 100644 --- a/gajim/application.py +++ b/gajim/application.py @@ -469,6 +469,7 @@ class GajimApplication(Gtk.Application): ('-archive', a.on_mam_preferences, 'feature', 's'), ('-sync-history', a.on_history_sync, 'online', 's'), ('-privacylists', a.on_privacy_lists, 'feature', 's'), + ('-blocking', a.on_blocking_list, 'feature', 's'), ('-send-server-message', a.on_send_server_message, 'online', 's'), ('-set-motd', a.on_set_motd, 'online', 's'), ('-update-motd', a.on_update_motd, 'online', 's'), @@ -517,3 +518,6 @@ class GajimApplication(Gtk.Application): elif event.feature == nbxmpp.NS_PRIVACY: action = '%s-privacylists' % event.account self.lookup_action(action).set_enabled(True) + elif event.feature == nbxmpp.NS_BLOCKING: + action = '%s-blocking' % event.account + self.lookup_action(action).set_enabled(True) diff --git a/gajim/common/modules/blocking.py b/gajim/common/modules/blocking.py index 1b208d29a..d0947e0a9 100644 --- a/gajim/common/modules/blocking.py +++ b/gajim/common/modules/blocking.py @@ -15,15 +15,26 @@ # XEP-0191: Blocking Command import logging +from functools import wraps import nbxmpp from gajim.common import app +from gajim.common.nec import NetworkEvent from gajim.common.nec import NetworkIncomingEvent log = logging.getLogger('gajim.c.m.blocking') +def ensure_online(func): + @wraps(func) + def func_wrapper(self, *args, **kwargs): + if not app.account_is_connected(self._account): + return + return func(self, *args, **kwargs) + return func_wrapper + + class Blocking: def __init__(self, con): self._con = con @@ -37,37 +48,48 @@ class Blocking: self.supported = False + self._nbmxpp_methods = [ + 'block', + 'unblock', + ] + + def __getattr__(self, key): + if key not in self._nbmxpp_methods: + raise AttributeError + if not app.account_is_connected(self._account): + log.warning('Account %s not connected, cant use %s', + self._account, key) + return + module = self._con.connection.get_module('Blocking') + return getattr(module, key) + def pass_disco(self, from_, _identities, features, _data, _node): if nbxmpp.NS_BLOCKING not in features: return self.supported = True + app.nec.push_incoming_event( + NetworkEvent('feature-discovered', + account=self._account, + feature=nbxmpp.NS_BLOCKING)) + log.info('Discovered blocking: %s', from_) - def get_blocking_list(self) -> None: - if not self.supported: - return - iq = nbxmpp.Iq('get', nbxmpp.NS_BLOCKING) - iq.setQuery('blocklist') + @ensure_online + def get_blocking_list(self, callback=None): log.info('Request list') - self._con.connection.SendAndCallForResponse( - iq, self._blocking_list_received) + if callback is None: + callback = self._blocking_list_received - def _blocking_list_received(self, stanza: nbxmpp.Iq) -> None: - if not nbxmpp.isResultNode(stanza): - log.info('Error: %s', stanza.getError()) + self._con.connection.get_module('Blocking').get_blocking_list( + callback=callback) + + def _blocking_list_received(self, result): + if result.is_error: + log.info('Error: %s', result.error) return - self.blocked = [] - blocklist = stanza.getTag('blocklist', namespace=nbxmpp.NS_BLOCKING) - if blocklist is None: - log.error('No blocklist node') - return - - for item in blocklist.getTags('item'): - self.blocked.append(item.getAttr('jid')) - log.info('Received list: %s', self.blocked) - + self.blocked = result.blocking_list app.nec.push_incoming_event( BlockingEvent(None, conn=self._con, changed=self.blocked)) @@ -129,35 +151,6 @@ class Blocking: probe = nbxmpp.Presence(jid, 'probe', frm=self._con.get_own_jid()) self._con.connection.send(probe) - def block(self, contact_list): - if not self.supported: - return - iq = nbxmpp.Iq('set', nbxmpp.NS_BLOCKING) - query = iq.setQuery(name='block') - - for contact in contact_list: - query.addChild(name='item', attrs={'jid': contact.jid}) - log.info('Block: %s', contact.jid) - self._con.connection.SendAndCallForResponse( - iq, self._default_result_handler, {}) - - def unblock(self, contact_list): - if not self.supported: - return - iq = nbxmpp.Iq('set', nbxmpp.NS_BLOCKING) - query = iq.setQuery(name='unblock') - - for contact in contact_list: - query.addChild(name='item', attrs={'jid': contact.jid}) - log.info('Unblock: %s', contact.jid) - self._con.connection.SendAndCallForResponse( - iq, self._default_result_handler, {}) - - @staticmethod - def _default_result_handler(_con, stanza): - if not nbxmpp.isResultNode(stanza): - log.warning('Operation failed: %s', stanza.getError()) - class BlockingEvent(NetworkIncomingEvent): name = 'blocking' diff --git a/gajim/common/modules/privacylists.py b/gajim/common/modules/privacylists.py index 0837ea407..c69746733 100644 --- a/gajim/common/modules/privacylists.py +++ b/gajim/common/modules/privacylists.py @@ -304,7 +304,8 @@ class PrivacyLists: def block_contacts(self, contact_list, message): if not self.supported: - self._con.get_module('Blocking').block(contact_list) + jid_list = [contact.jid for contact in contact_list] + self._con.get_module('Blocking').block(jid_list) return if self.default_list is None: @@ -350,7 +351,8 @@ class PrivacyLists: def unblock_contacts(self, contact_list): if not self.supported: - self._con.get_module('Blocking').unblock(contact_list) + jid_list = [contact.jid for contact in contact_list] + self._con.get_module('Blocking').unblock(jid_list) return new_blocked_list = [] diff --git a/gajim/data/gui/blocking_list.ui b/gajim/data/gui/blocking_list.ui new file mode 100644 index 000000000..ff3310868 --- /dev/null +++ b/gajim/data/gui/blocking_list.ui @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.12"/> + <object class="GtkListStore" id="blocking_store"> + <columns> + <!-- column-name jid --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkGrid" id="blocking_grid"> + <property name="width_request">400</property> + <property name="height_request">300</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">18</property> + <property name="margin_right">18</property> + <property name="margin_top">18</property> + <property name="margin_bottom">18</property> + <property name="row_spacing">5</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkButtonBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="spacing">5</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="add_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="_on_add" swapped="no"/> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-add-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + <property name="non_homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkButton" id="remove_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="_on_remove" swapped="no"/> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-remove-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + <property name="non_homogeneous">True</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="save_button"> + <property name="label">Save</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">end</property> + <property name="always_show_image">True</property> + <signal name="clicked" handler="_on_save" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkOverlay" id="overlay"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkScrolledWindow"> + <property name="height_request">150</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="block_view"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="model">blocking_store</property> + <property name="search_column">0</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeview-selection2"/> + </child> + <child> + <object class="GtkTreeViewColumn" id="treeviewcolumn1"> + <property name="title" translatable="yes">Jabber ID</property> + <property name="expand">True</property> + <property name="clickable">True</property> + <property name="sort_indicator">True</property> + <property name="sort_column_id">0</property> + <child> + <object class="GtkCellRendererText" id="cellrenderertext3"> + <property name="editable">True</property> + <property name="placeholder_text">user@example.org</property> + <signal name="edited" handler="_jid_edited" swapped="no"/> + </object> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="index">-1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + <property name="width">2</property> + </packing> + </child> + </object> +</interface> diff --git a/gajim/gtk/blocking.py b/gajim/gtk/blocking.py new file mode 100644 index 000000000..8b5d53322 --- /dev/null +++ b/gajim/gtk/blocking.py @@ -0,0 +1,150 @@ +# 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/>. + +import logging + +from gi.repository import Gtk +from gi.repository import Gdk + +from gajim.common import app +from gajim.common.i18n import _ + +from gajim.gtk.util import get_builder +from gajim.gtk.dialogs import HigDialog + +log = logging.getLogger('gajim.gtk.blocking_list') + + +class BlockingList(Gtk.ApplicationWindow): + def __init__(self, account): + Gtk.ApplicationWindow.__init__(self) + self.set_application(app.app) + self.set_position(Gtk.WindowPosition.CENTER) + self.set_show_menubar(False) + self.set_title(_('Blocking List for %s') % account) + + self.connect('key-press-event', self._on_key_press) + + self.account = account + self._con = app.connections[account] + self._prev_blocked_jids = set() + self._await_results = 2 + self._received_errors = False + + self._ui = get_builder('blocking_list.ui') + self.add(self._ui.blocking_grid) + + self._spinner = Gtk.Spinner() + self._ui.overlay.add_overlay(self._spinner) + + self._set_grid_state(False) + self._ui.connect_signals(self) + self.show_all() + + self._activate_spinner() + + self._con.get_module('Blocking').get_blocking_list( + callback=self._on_blocking_list_received) + + def _reset_after_error(self): + self._received_errors = False + self._await_results = 2 + self._disable_spinner() + self._set_grid_state(True) + + def _show_error(self, error): + dialog = HigDialog( + self, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, + _('Error!'), + error) + dialog.popup() + + def _on_blocking_list_received(self, result): + self._disable_spinner() + self._set_grid_state(not result.is_error) + + if result.is_error: + self._show_error(result.error) + + else: + self._prev_blocked_jids = set(result.blocking_list) + self._ui.blocking_store.clear() + for item in result.blocking_list: + self._ui.blocking_store.append((item,)) + + def _on_save_result(self, result): + self._await_results -= 1 + if result.is_error and not self._received_errors: + self._show_error(result.error) + self._received_errors = True + + if not self._await_results: + if self._received_errors: + self._reset_after_error() + else: + self.destroy() + + def _set_grid_state(self, state): + self._ui.blocking_grid.set_sensitive(state) + + def _jid_edited(self, _renderer, path, new_text): + iter_ = self._ui.blocking_store.get_iter(path) + self._ui.blocking_store.set_value(iter_, 0, new_text) + + def _on_add(self, _button): + self._ui.blocking_store.append(['']) + + def _on_remove(self, _button): + mod, paths = self._ui.block_view.get_selection().get_selected_rows() + for path in paths: + iter_ = mod.get_iter(path) + self._ui.blocking_store.remove(iter_) + + def _on_save(self, _button): + self._activate_spinner() + self._set_grid_state(False) + + blocked_jids = set() + for item in self._ui.blocking_store: + blocked_jids.add(item[0].lower()) + + unblock_jids = self._prev_blocked_jids - blocked_jids + if unblock_jids: + self._con.get_module('Blocking').unblock( + unblock_jids, callback=self._on_save_result) + else: + self._await_results -= 1 + + block_jids = blocked_jids - self._prev_blocked_jids + if block_jids: + self._con.get_module('Blocking').block( + block_jids, callback=self._on_save_result) + else: + self._await_results -= 1 + + if not self._await_results: + # No changes + self.destroy() + + def _activate_spinner(self): + self._spinner.show() + self._spinner.start() + + def _disable_spinner(self): + self._spinner.hide() + self._spinner.stop() + + def _on_key_press(self, _widget, event): + if event.keyval == Gdk.KEY_Escape: + self.destroy() diff --git a/gajim/gui_menu_builder.py b/gajim/gui_menu_builder.py index 3cbc7f1d3..ef67df5ff 100644 --- a/gajim/gui_menu_builder.py +++ b/gajim/gui_menu_builder.py @@ -788,6 +788,7 @@ def get_account_menu(account): ('-start-single-chat', _('Send Single Messageā¦')), (_('Advanced'), [ ('-archive', _('Archiving Preferences')), + ('-blocking', _('Blocking List')), ('-sync-history', _('Synchronise History')), ('-privacylists', _('Privacy Lists')), ('-server-info', _('Server Info')),