Add blocking list dialog

This commit is contained in:
Philipp Hörist 2019-01-21 22:45:19 +01:00
parent b0742377f0
commit 1a7d930fc4
7 changed files with 358 additions and 51 deletions

View File

@ -44,6 +44,7 @@ from gajim.gtk.history import HistoryWindow
from gajim.gtk.accounts import AccountsWindow from gajim.gtk.accounts import AccountsWindow
from gajim.gtk.proxies import ManageProxies from gajim.gtk.proxies import ManageProxies
from gajim.gtk.discovery import ServiceDiscoveryWindow from gajim.gtk.discovery import ServiceDiscoveryWindow
from gajim.gtk.blocking import BlockingList
# General Actions # General Actions
@ -211,6 +212,15 @@ def on_mam_preferences(action, param):
window.present() 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): def on_history_sync(action, param):
account = param.get_string() account = param.get_string()
if 'history_sync' in interface.instances[account]: if 'history_sync' in interface.instances[account]:

View File

@ -469,6 +469,7 @@ class GajimApplication(Gtk.Application):
('-archive', a.on_mam_preferences, 'feature', 's'), ('-archive', a.on_mam_preferences, 'feature', 's'),
('-sync-history', a.on_history_sync, 'online', 's'), ('-sync-history', a.on_history_sync, 'online', 's'),
('-privacylists', a.on_privacy_lists, 'feature', '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'), ('-send-server-message', a.on_send_server_message, 'online', 's'),
('-set-motd', a.on_set_motd, 'online', 's'), ('-set-motd', a.on_set_motd, 'online', 's'),
('-update-motd', a.on_update_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: elif event.feature == nbxmpp.NS_PRIVACY:
action = '%s-privacylists' % event.account action = '%s-privacylists' % event.account
self.lookup_action(action).set_enabled(True) 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)

View File

@ -15,15 +15,26 @@
# XEP-0191: Blocking Command # XEP-0191: Blocking Command
import logging import logging
from functools import wraps
import nbxmpp import nbxmpp
from gajim.common import app from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.nec import NetworkIncomingEvent from gajim.common.nec import NetworkIncomingEvent
log = logging.getLogger('gajim.c.m.blocking') 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: class Blocking:
def __init__(self, con): def __init__(self, con):
self._con = con self._con = con
@ -37,37 +48,48 @@ class Blocking:
self.supported = False 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): def pass_disco(self, from_, _identities, features, _data, _node):
if nbxmpp.NS_BLOCKING not in features: if nbxmpp.NS_BLOCKING not in features:
return return
self.supported = True self.supported = True
app.nec.push_incoming_event(
NetworkEvent('feature-discovered',
account=self._account,
feature=nbxmpp.NS_BLOCKING))
log.info('Discovered blocking: %s', from_) log.info('Discovered blocking: %s', from_)
def get_blocking_list(self) -> None: @ensure_online
if not self.supported: def get_blocking_list(self, callback=None):
return
iq = nbxmpp.Iq('get', nbxmpp.NS_BLOCKING)
iq.setQuery('blocklist')
log.info('Request list') log.info('Request list')
self._con.connection.SendAndCallForResponse( if callback is None:
iq, self._blocking_list_received) callback = self._blocking_list_received
def _blocking_list_received(self, stanza: nbxmpp.Iq) -> None: self._con.connection.get_module('Blocking').get_blocking_list(
if not nbxmpp.isResultNode(stanza): callback=callback)
log.info('Error: %s', stanza.getError())
def _blocking_list_received(self, result):
if result.is_error:
log.info('Error: %s', result.error)
return return
self.blocked = [] self.blocked = result.blocking_list
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)
app.nec.push_incoming_event( app.nec.push_incoming_event(
BlockingEvent(None, conn=self._con, changed=self.blocked)) 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()) probe = nbxmpp.Presence(jid, 'probe', frm=self._con.get_own_jid())
self._con.connection.send(probe) 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): class BlockingEvent(NetworkIncomingEvent):
name = 'blocking' name = 'blocking'

View File

@ -304,7 +304,8 @@ class PrivacyLists:
def block_contacts(self, contact_list, message): def block_contacts(self, contact_list, message):
if not self.supported: 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 return
if self.default_list is None: if self.default_list is None:
@ -350,7 +351,8 @@ class PrivacyLists:
def unblock_contacts(self, contact_list): def unblock_contacts(self, contact_list):
if not self.supported: 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 return
new_blocked_list = [] new_blocked_list = []

View File

@ -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>

150
gajim/gtk/blocking.py Normal file
View File

@ -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()

View File

@ -788,6 +788,7 @@ def get_account_menu(account):
('-start-single-chat', _('Send Single Message…')), ('-start-single-chat', _('Send Single Message…')),
(_('Advanced'), [ (_('Advanced'), [
('-archive', _('Archiving Preferences')), ('-archive', _('Archiving Preferences')),
('-blocking', _('Blocking List')),
('-sync-history', _('Synchronise History')), ('-sync-history', _('Synchronise History')),
('-privacylists', _('Privacy Lists')), ('-privacylists', _('Privacy Lists')),
('-server-info', _('Server Info')), ('-server-info', _('Server Info')),