add instance ban popover for admins

This commit is contained in:
Izalia Mae 2022-11-23 13:36:45 -05:00
parent eabc8084de
commit 33cdd8f7fc
6 changed files with 580 additions and 25 deletions

View file

@ -1,12 +1,16 @@
import asyncio import asyncio
import json
from izzylib.misc import random_str from izzylib.misc import random_str
from mastodon.Mastodon import MastodonAPIError
from pprint import pprint
from threading import Thread
from .. import cache, var from .. import cache, var
from ..base import ComponentBase from ..base import ComponentBase
from ..database import default_permissions from ..database import default_permissions
from ..exceptions import AccountNotFoundError, NoAccountsError from ..exceptions import AccountNotFoundError, NoAccountsError
from ..functions import connect, get_buffer_text from ..functions import connect, get_buffer_text, run_in_gui_thread
from ..objects import SavedLoginRow from ..objects import SavedLoginRow
@ -32,11 +36,13 @@ class StatusBar(ComponentBase):
self.bookmark_row = None self.bookmark_row = None
self.theme_enabled = False self.theme_enabled = False
self.toot_acct = None self.toot_account = None
self.toot_post = None self.toot_post = None
self.toot_max_len = 500 self.toot_max_len = 500
self.unsaved_logins = [] self.unsaved_logins = []
self.fediban_bans = dict()
self.fediban_current = None
self.setup() self.setup()
@ -85,6 +91,57 @@ class StatusBar(ComponentBase):
self.bookmark_fields[k].set_text(value) self.bookmark_fields[k].set_text(value)
def fediban_get(self, domain):
self.fediban_refresh(check_only=True)
try:
return self.fediban_bans[domain]
except KeyError:
pass
for row in self.fediban_bans.values():
if row.domain in domain or domain in row.domain:
return row
def fediban_get_fields(self):
return DotDict(
domain = self['fediban-domain'].get_text(),
severity = self['fediban-severity'].get_active_id(),
private_comment = get_buffer_text(self['fediban-private']),
public_comment = get_buffer_text(self['fediban-public']),
nomedia = self['fediban-media'].get_active(),
noreports = self['fediban-reports'].get_active(),
obfuscate = self['fediban-obfuscate'].get_active()
)
def fediban_set_fields(self, row=None):
self['fediban-domain'].set_text(row.domain if row else self.tab.url.domain)
self['fediban-public'].get_buffer().set_text(row.public_comment if row else '')
self['fediban-private'].get_buffer().set_text(row.private_comment if row else '')
self['fediban-severity'].set_active_id(row.severity if row else 'silence')
self['fediban-media'].set_active(row.reject_media if row else False)
self['fediban-reports'].set_active(row.reject_reports if row else False)
self['fediban-obfuscate'].set_active(row.obfuscate if row else False)
self['fediban-domain'].set_sensitive(False if row else True)
self['fediban-delete'].set_sensitive(True if row else False)
self['fediban-save'].set_label('Update' if row else 'Save')
def fediban_refresh(self, force=False, check_only=False):
if not self.fediban_bans or force:
self.fediban_bans = self.toot_account.api.admin_domains_all(200)
if check_only:
return
self.fediban_current = row = self.fediban_get(self.tab.url.domain)
run_in_gui_thread(self.fediban_set_fields, row)
def login_unsaved_del_row(self, row): def login_unsaved_del_row(self, row):
self.logins.unsaved.remove(row['container']) self.logins.unsaved.remove(row['container'])
self.unsaved_logins.remove(row) self.unsaved_logins.remove(row)
@ -218,14 +275,12 @@ class StatusBar(ComponentBase):
tab = self.tab tab = self.tab
if name == 'debug': if name == 'debug':
#self.window.notification('Merp!', 'INFO', timeout=0, system=True) self.toot_account = self.app.get_default_account()
print(asyncio.get_running_loop()) data = self.toot_account.api.admin_domains_all()
print(len(data))
#if self.window.themes.current: # self.fediban_refresh()
#self.window.themes.unset() # for row in self.fediban_bans:
# print('gab.com' in row.domain, row.domain)
#else:
#self.window.themes.set('test')
elif name == 'bookmark': elif name == 'bookmark':
if not tab.url: if not tab.url:
@ -287,6 +342,24 @@ class StatusBar(ComponentBase):
self['toot-content'].grab_focus() self['toot-content'].grab_focus()
elif name == 'fediban':
try:
self.toot_account = self.app.get_default_account()
except NoAccountsError:
self.window.notification('No active fedi accounts', 'error')
return self['fediban-popover'].popdown()
try:
Thread(target=self.fediban_refresh).start()
except MastodonAPIError as e:
if e.args[0] == 'Mastodon API returned error' and e.args[1] == 403:
self.window.notification('Admin actions not available on this account')
return self['fediban-popover'].popdown()
raise e
def handle_bookmark_button(self, name): def handle_bookmark_button(self, name):
with self.db.session as s: with self.db.session as s:
@ -369,6 +442,43 @@ class StatusBar(ComponentBase):
cache.posts.store(self.tab.url, post) cache.posts.store(self.tab.url, post)
def handle_fediban(self, action):
if not self.toot_account:
return
acct = self.toot_account
row = self.fediban_current
if action == 'hide':
self.fediban_set_fields()
elif action == 'save':
new_data = self.fediban_get_fields()
new_row = None
if row == None:
new_row = acct.api.admin_domains_block(**new_data)
self.window.notification(f'Blocked domain: {new_row.domain}')
else:
del new_data['domain']
new_row = acct.api.admin_domains_update(row.id, **new_data)
self.window.notification(f'Updated domain block: {row.domain}')
self.fediban_bans[new_row.domain] = new_row
elif action == 'delete':
acct.api.admin_domains_unblock(row.id)
del row.domain
self.window.notification(f'Unblocked domain: {row.domain}')
elif action == 'refresh':
Thread(target=self.fediban_refresh, kwargs={'force': True}).start()
if action in {'save', 'delete', 'close'}:
self['fediban-popover'].popdown()
def handle_siteoptions_switch(self, name): def handle_siteoptions_switch(self, name):
active = self[f'siteoptions-{name}'].get_active() active = self[f'siteoptions-{name}'].get_active()
try: try:
@ -484,6 +594,14 @@ class StatusBar(ComponentBase):
self.connect('toot-content', 'key-press-event', self.handle_toot_key_press) self.connect('toot-content', 'key-press-event', self.handle_toot_key_press)
connect(self['toot-content'].get_buffer(), 'changed', self.toot_update_count) connect(self['toot-content'].get_buffer(), 'changed', self.toot_update_count)
## FediBan
self.connect('fediban-popover', 'show', self.handle_status_button, 'fediban')
self.connect('fediban-popover', 'hide', self.handle_fediban, 'hide')
self.connect('fediban-delete', 'clicked', self.handle_fediban, 'delete')
self.connect('fediban-save', 'clicked', self.handle_fediban, 'save')
self.connect('fediban-refresh', 'clicked', self.handle_fediban, 'refresh')
self.connect('fediban-close', 'clicked', self.handle_fediban, 'close')
## Logins ## Logins
self.connect('logins-close', 'clicked', self.handle_logins_button, 'close') self.connect('logins-close', 'clicked', self.handle_logins_button, 'close')
self.connect('logins-unsaved-clear', 'clicked', self.handle_logins_button, 'clear') self.connect('logins-unsaved-clear', 'clicked', self.handle_logins_button, 'clear')

View file

@ -80,6 +80,7 @@ class WebviewHandler(ComponentBase):
def handle_context_menu(self, webview, context_menu, event, hit): def handle_context_menu(self, webview, context_menu, event, hit):
account = self.app.get_default_account()
menu = ContextMenuClass(context_menu, self.tab) menu = ContextMenuClass(context_menu, self.tab)
url = DotDict( url = DotDict(
page = webview.get_uri(), page = webview.get_uri(),
@ -103,6 +104,8 @@ class WebviewHandler(ComponentBase):
url[key] = Url(value) url[key] = Url(value)
with self.app.db.session as s:
permissions = s.get_permission(url.page.hostname())
if data.link: if data.link:
menu.new_action('link_open', 'Open Link', self.tab.load_url, url.link) menu.new_action('link_open', 'Open Link', self.tab.load_url, url.link)

View file

@ -3,11 +3,12 @@ import re, traceback
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
from izzylib_sql import Row from izzylib_sql import Row
from mastodon import Mastodon # from mastodon import Mastodon
from PIL import Image from PIL import Image
from urllib.parse import quote_plus from urllib.parse import quote_plus
from ..functions import TimeoutCallback, get_app, run_in_gui_thread from ..functions import TimeoutCallback, get_app, run_in_gui_thread
from ..mastodon_temp import Mastodon
row_classes = {} row_classes = {}
@ -37,7 +38,8 @@ class Account(RowBase):
@property @property
def api(self): def api(self):
if not self._api: if not self._api:
self._set_api() self._api = Mastodon(access_token=self.token, api_base_url=f'https://{self.domain}')
logging.debug(f'logged into {self.fullhandle}')
return self._api return self._api
@ -80,11 +82,6 @@ class Account(RowBase):
return f'{self.handle}@{self.domain}' return f'{self.handle}@{self.domain}'
def _set_api(self):
self._api = Mastodon(access_token=self.token, api_base_url=f'https://{self.domain}')
logging.debug(f'logged into {self.fullhandle}')
def fetch_avatar(self): def fetch_avatar(self):
byte = BytesIO() byte = BytesIO()
http_client = get_app().http_client http_client = get_app().http_client

View file

@ -94,6 +94,9 @@ def get_app():
def get_buffer_text(text_buffer): def get_buffer_text(text_buffer):
if not isinstance(text_buffer, Gtk.TextBuffer):
text_buffer = text_buffer.get_buffer()
return text_buffer.get_text(text_buffer.get_start_iter(), text_buffer.get_end_iter(), True) return text_buffer.get_text(text_buffer.get_start_iter(), text_buffer.get_end_iter(), True)

View file

@ -0,0 +1,129 @@
from mastodon import Mastodon as MastodonPy
from mastodon.Mastodon import api_version
class Mastodon(MastodonPy):
__DICT_VERSION_ADMIN_DOMAIN = '4.0.0'
def __init__(self, *args, **kwargs):
MastodonPy.__init__(self, *args, **kwargs)
self._patch()
def _patch(self):
self._Mastodon__SCOPE_SETS['admin:read'].extend([
'admin:read:domain_allows',
'admin:read:domain_blocks',
'admin:read:ip_blocks',
'admin:read:email_domain_blocks',
'admin:read:cononical_email_blocks'
])
self._Mastodon__SCOPE_SETS['admin:write'].extend([
'admin:read:domain_allows',
'admin:read:domain_blocks',
'admin:read:ip_blocks',
'admin:read:email_domain_blocks',
'admin:read:cononical_email_blocks'
])
def __api_request(self, *args, **kwargs):
data = MastodonPy.__api_request(self, *args, **kwargs)
if isinstance(data, list):
return [DotDict(row) for row in data]
elif isinstance(data, dict):
return DotDict(data)
return data
@api_version('4.0.0', '4.0.0', __DICT_VERSION_ADMIN_DOMAIN)
def admin_domains(self, limit=100, max_id=None):
'heck'
params = {'limit': limit}
if max_id != None:
params['max_id'] = max_id
return self.__api_request('GET', '/api/v1/admin/domain_blocks', params, use_json=True)
@api_version('4.0.0', '4.0.0', __DICT_VERSION_ADMIN_DOMAIN)
def admin_domains_blocked(self, id):
'heck'
return self.__api_request('GET', f'/api/v1/admin/domain_blocks/{id}')
@api_version('4.0.0', '4.0.0', __DICT_VERSION_ADMIN_DOMAIN)
def admin_domains_block(self, domain, severity='silence', nomedia=False, noreports=False, private_comment=None, public_comment=None, obfuscate=False):
'heck'
params = {
'domain': domain,
'severity': severity,
'reject_media': nomedia,
'reject_reports': noreports,
'private_comment': private_comment,
'public_comment': public_comment,
'obfuscate': obfuscate
}
return self.__api_request('POST', '/api/v1/admin/domain_blocks', params, use_json=True)
@api_version('4.0.0', '4.0.0', __DICT_VERSION_ADMIN_DOMAIN)
def admin_domains_update(self, id, severity='silence', nomedia=None, noreports=None, private_comment=None, public_comment=None, obfuscate=None):
'heck'
params = {}
if severity:
params['severity'] = severity
if nomedia != None:
params['reject_media'] = nomedia
if noreports != None:
params['reject_reports'] = noreports
if obfuscate != None:
params['obfuscate'] = obfuscate
if private_comment != None:
params['private_comment'] = private_comment
if public_comment != None:
params['public_comment'] = public_comment
return self.__api_request('PUT', f'/api/v1/admin/domain_blocks/{id}', params, use_json=True)
@api_version('4.0.0', '4.0.0', __DICT_VERSION_ADMIN_DOMAIN)
def admin_domains_unblock(self, id):
'heck'
return self.__api_request('DELETE', f'/api/v1/admin/domain_blocks/{id}')
def admin_domains_all(self, limit=100):
bans = dict()
new_bans = []
offset = None
while True:
if offset == None:
new_bans = self.admin_domains(limit)
else:
new_bans = self.admin_domains(limit, offset)
for row in new_bans:
bans[row.domain] = row
offset = new_bans[-1].id
if len(new_bans) < limit:
break
logging.verbose(f'Fetched {len(bans)} domain ban(s)')
return bans

View file

@ -411,6 +411,16 @@
<property name="pixel-size">20</property> <property name="pixel-size">20</property>
<property name="icon-name">mail-mark-notjunk</property> <property name="icon-name">mail-mark-notjunk</property>
</object> </object>
<object class="GtkImage" id="statusbar-fediban-close-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">window-close</property>
</object>
<object class="GtkImage" id="statusbar-fediban-refresh-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">view-refresh</property>
</object>
<object class="GtkImage" id="statusbar-logins-close-icon"> <object class="GtkImage" id="statusbar-logins-close-icon">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property> <property name="can-focus">False</property>
@ -1576,6 +1586,24 @@
<property name="position">2</property> <property name="position">2</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkMenuButton" id="statusbar-fediban">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="relief">none</property>
<property name="popover">statusbar-fediban-popover</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child> <child>
<object class="GtkMenuButton" id="statusbar-toot"> <object class="GtkMenuButton" id="statusbar-toot">
<property name="visible">True</property> <property name="visible">True</property>
@ -1595,7 +1623,7 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">3</property> <property name="position">4</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -1615,7 +1643,7 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">4</property> <property name="position">5</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -1635,7 +1663,7 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">5</property> <property name="position">6</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -1655,7 +1683,7 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">6</property> <property name="position">7</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -1666,7 +1694,7 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">7</property> <property name="position">8</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -1687,7 +1715,7 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">8</property> <property name="position">9</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -1709,7 +1737,7 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">9</property> <property name="position">10</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -1731,7 +1759,7 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">10</property> <property name="position">11</property>
</packing> </packing>
</child> </child>
<style> <style>
@ -1800,4 +1828,281 @@
</object> </object>
</child> </child>
</object> </object>
<object class="GtkPopover" id="statusbar-fediban-popover">
<property name="can-focus">False</property>
<property name="relative-to">statusbar-fediban</property>
<child>
<!-- n-columns=2 n-rows=11 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="has-tooltip">True</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="row-spacing">5</property>
<property name="column-spacing">5</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="statusbar-fediban-refresh">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Do a full refresh of banned accounts</property>
<property name="image">statusbar-fediban-refresh-icon</property>
<property name="relief">none</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Instance Ban</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="statusbar-fediban-close">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Close popover</property>
<property name="image">statusbar-fediban-close-icon</property>
<property name="relief">none</property>
<property name="always-show-image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="statusbar-fediban-domain">
<property name="width-request">300</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="tooltip-text" translatable="yes">Domain to ban. Remove sub-domain to ban the domain and all sub-domains.</property>
<property name="placeholder-text" translatable="yes">Domain</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkTextView" id="statusbar-fediban-private">
<property name="height-request">75</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="tooltip-text" translatable="yes">Comment that will only be visible to other admins and mods</property>
<property name="left-margin">5</property>
<property name="right-margin">5</property>
<property name="top-margin">5</property>
<property name="bottom-margin">5</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">6</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkTextView" id="statusbar-fediban-public">
<property name="height-request">75</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="tooltip-text" translatable="yes">Comment that will be visible to everybody</property>
<property name="left-margin">5</property>
<property name="right-margin">5</property>
<property name="top-margin">5</property>
<property name="bottom-margin">5</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">4</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="statusbar-fediban-severity">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">The severity of the ban</property>
<property name="active">1</property>
<items>
<item id="suspend" translatable="yes">Suspend</item>
<item id="silence" translatable="yes">Silence</item>
<item id="noop" translatable="yes">None</item>
</items>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Obfuscate</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">9</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="statusbar-fediban-obfuscate">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="tooltip-text" translatable="yes">Replace some characters in the domain name with *'s when displayed on the about page</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">9</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Reject Reports</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">8</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="statusbar-fediban-reports">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="tooltip-text" translatable="yes">Ignore any reports coming from the domain</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">8</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Reject Media</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">7</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="statusbar-fediban-media">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="tooltip-text" translatable="yes">Don't cache any media coming from the domain</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">7</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Public Comment</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">3</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Private Comment</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">5</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<property name="homogeneous">True</property>
<child>
<object class="GtkButton" id="statusbar-fediban-delete">
<property name="label" translatable="yes">Delete</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="statusbar-fediban-save">
<property name="label" translatable="yes">Save</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">10</property>
<property name="width">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface> </interface>