Merge branch 'master' into 'gnotification'
# Conflicts: # gajim/gajim.py
|
@ -3,7 +3,7 @@
|
|||
CONF_ARGS=""
|
||||
|
||||
echo "[encoding: UTF-8]" > po/POTFILES.in \
|
||||
&& for p in `ls data/gui/*.ui`; do echo "[type: gettext/glade]$p" >> \
|
||||
&& for p in `ls gajim/data/gui/*.ui`; do echo "[type: gettext/glade]$p" >> \
|
||||
po/POTFILES.in; done \
|
||||
&& ls -1 data/org.gajim.Gajim.appdata.xml.in data/org.gajim.Gajim.desktop.in.in data/gajim-remote.desktop.in.in \
|
||||
gajim/*.py gajim/common/*.py gajim/command_system/*.py gajim/command_system/implementation/*.py gajim/common/zeroconf/*.py gajim/plugins/*.py | grep -v ipython_view.py >> \
|
||||
|
|
11
configure.ac
|
@ -54,12 +54,13 @@ AC_SUBST(PACKAGE)
|
|||
AC_CONFIG_FILES([
|
||||
Makefile
|
||||
data/Makefile
|
||||
data/gui/Makefile
|
||||
data/emoticons/Makefile
|
||||
gajim/data/Makefile
|
||||
gajim/data/gui/Makefile
|
||||
gajim/data/emoticons/Makefile
|
||||
data/pixmaps/Makefile
|
||||
data/iconsets/Makefile
|
||||
data/moods/Makefile
|
||||
data/activities/Makefile
|
||||
gajim/data/iconsets/Makefile
|
||||
gajim/data/moods/Makefile
|
||||
gajim/data/activities/Makefile
|
||||
icons/Makefile
|
||||
data/org.gajim.Gajim.appdata.xml
|
||||
data/org.gajim.Gajim.desktop.in
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
SUBDIRS = gui emoticons pixmaps iconsets moods activities
|
||||
SUBDIRS = pixmaps
|
||||
@INTLTOOL_DESKTOP_RULE@
|
||||
|
||||
appstreamdir = $(datadir)/metainfo/
|
||||
|
@ -13,25 +13,12 @@ desktop_DATA = $(desktop_in_files:.desktop.in.in=.desktop)
|
|||
installdefsdir = $(gajim_srcdir)/common
|
||||
installdefs_DATA = defs.py
|
||||
|
||||
soundsdir = $(pkgdatadir)/data/sounds
|
||||
sounds_DATA = $(srcdir)/sounds/*.wav
|
||||
|
||||
styledir = $(pkgdatadir)/data/style
|
||||
style_DATA = $(srcdir)/style/*.css
|
||||
|
||||
otherdir = $(pkgdatadir)/data/other
|
||||
other_DATA = other/servers.xml other/dh4096.pem
|
||||
# other/cacert.pem is used only on Windows. On Unix platforms
|
||||
# use CA certificates installed in /etc/ssl/certs
|
||||
|
||||
man_MANS = gajim.1 gajim-remote.1 gajim-history-manager.1
|
||||
|
||||
|
||||
EXTRA_DIST = $(appstream_in_files) \
|
||||
$(desktop_in_files) \
|
||||
$(sounds_DATA) \
|
||||
$(style_DATA) \
|
||||
$(other_DATA) \
|
||||
$(man_MANS) \
|
||||
defs.py.in
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ Where to look for logs file
|
|||
.El
|
||||
.Sh FILES
|
||||
.Bl -tag -width Ds
|
||||
.It $XDG_DATA_DIR/gajim/logs.db
|
||||
.It $XDG_DATA_HOME/gajim/logs.db
|
||||
The history database log file used when
|
||||
.Op Fl c
|
||||
is not specified.
|
||||
|
|
12
data/gajim.1
|
@ -66,19 +66,19 @@ Where to look for configuration files
|
|||
.El
|
||||
.Sh FILES
|
||||
.Bl -tag -width Ds
|
||||
.It $XDG_CACHE_DIR/gajim/cache.db
|
||||
.It $XDG_CACHE_HOME/gajim/cache.db
|
||||
Cache database file of transports, caps, roster entry, and roster group.
|
||||
.It $XDG_CACHE_DIR/gajim/avatars/
|
||||
.It $XDG_CACHE_HOME/gajim/avatars/
|
||||
Cache directory of avatars.
|
||||
.It $XDG_CACHE_DIR/gajim/vcards/
|
||||
.It $XDG_CACHE_HOME/gajim/vcards/
|
||||
Cache directory of vCards (virtual cards).
|
||||
.It $XDG_CONFIG_DIR/gajim/
|
||||
.It $XDG_CONFIG_HOME/gajim/
|
||||
The config-path used when
|
||||
.Op Fl c
|
||||
is not specified.
|
||||
.It $XDG_DATA_DIR/gajim/certs/
|
||||
.It $XDG_DATA_HOME/gajim/certs/
|
||||
Directory where certificates are stored.
|
||||
.It $XDG_DATA_DIR/gajim/logs.db
|
||||
.It $XDG_DATA_HOME/gajim/logs.db
|
||||
The history database log file.
|
||||
For more information on database logs see
|
||||
<https://dev.gajim.org/gajim/gajim/wikis/development/LogsDatabase>.
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
SUBDIRS = data
|
||||
|
||||
INCLUDES = \
|
||||
$(PYTHON_INCLUDES)
|
||||
export MACOSX_DEPLOYMENT_TARGET=10.4
|
||||
|
|
698
gajim/accounts_window.py
Normal file
|
@ -0,0 +1,698 @@
|
|||
from functools import partial
|
||||
|
||||
from gi.repository import Gtk, Gio, GLib, Gdk
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.gtkgui_helpers import get_image_button
|
||||
from gajim import gtkgui_helpers
|
||||
from gajim import gui_menu_builder
|
||||
from gajim.common import passwords
|
||||
from gajim import dialogs
|
||||
from gajim import config
|
||||
from gajim.common import helpers
|
||||
from gajim.common.connection import Connection
|
||||
from gajim.common.zeroconf.connection_zeroconf import ConnectionZeroconf
|
||||
from gajim.options_dialog import OptionsDialog, OptionsBox
|
||||
from gajim.common.const import Option, OptionKind, OptionType
|
||||
|
||||
|
||||
class AccountsWindow(Gtk.ApplicationWindow):
|
||||
def __init__(self):
|
||||
Gtk.ApplicationWindow.__init__(self)
|
||||
self.set_application(app.app)
|
||||
self.set_position(Gtk.WindowPosition.CENTER)
|
||||
self.set_show_menubar(False)
|
||||
self.set_name('AccountsWindow')
|
||||
self.set_size_request(500, -1)
|
||||
self.set_resizable(False)
|
||||
self.need_relogin = {}
|
||||
|
||||
glade_objects = [
|
||||
'stack', 'box', 'actionbar', 'headerbar', 'back_button',
|
||||
'menu_button', 'account_page', 'account_list']
|
||||
self.builder = gtkgui_helpers.get_gtk_builder('accounts_window.ui')
|
||||
for obj in glade_objects:
|
||||
setattr(self, obj, self.builder.get_object(obj))
|
||||
|
||||
self.set_titlebar(self.headerbar)
|
||||
|
||||
menu = Gio.Menu()
|
||||
menu.append('Merge Accounts', 'app.merge')
|
||||
menu.append('Use PGP Agent', 'app.agent')
|
||||
self.menu_button.set_menu_model(menu)
|
||||
|
||||
button = get_image_button('list-add-symbolic', 'Add')
|
||||
button.set_action_name('app.add-account')
|
||||
self.actionbar.pack_start(button)
|
||||
|
||||
accounts = app.config.get_per('accounts')
|
||||
accounts.sort()
|
||||
for account in accounts:
|
||||
self.need_relogin[account] = self.get_relogin_options(account)
|
||||
account_item = Account(account, self)
|
||||
self.account_list.add(account_item)
|
||||
account_item.set_activatable()
|
||||
|
||||
self.add(self.box)
|
||||
self.builder.connect_signals(self)
|
||||
|
||||
self.connect('destroy', self.on_destroy)
|
||||
self.connect('key-press-event', self.on_key_press)
|
||||
self.show_all()
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
self.destroy()
|
||||
|
||||
def on_destroy(self, *args):
|
||||
self.check_relogin()
|
||||
del app.interface.instances['accounts']
|
||||
|
||||
def on_child_visible(self, stack, *args):
|
||||
page = stack.get_visible_child_name()
|
||||
if page is None:
|
||||
return
|
||||
if page == 'main':
|
||||
self.menu_button.show()
|
||||
self.back_button.hide()
|
||||
self.check_relogin()
|
||||
else:
|
||||
self.back_button.show()
|
||||
self.menu_button.hide()
|
||||
|
||||
def on_back_button(self, *args):
|
||||
page = self.stack.get_visible_child_name()
|
||||
child = self.stack.get_visible_child()
|
||||
self.remove_all_pages()
|
||||
if page == 'account':
|
||||
child.toggle.set_active(False)
|
||||
self.stack.add_named(self.account_page, 'main')
|
||||
self.stack.set_visible_child_name('main')
|
||||
self.update_accounts()
|
||||
else:
|
||||
self.stack.add_named(child.parent, 'account')
|
||||
self.stack.set_visible_child_name('account')
|
||||
|
||||
def update_accounts(self):
|
||||
for row in self.account_list.get_children():
|
||||
row.get_child().update()
|
||||
|
||||
@staticmethod
|
||||
def on_row_activated(listbox, row):
|
||||
row.get_child().on_row_activated()
|
||||
|
||||
def remove_all_pages(self):
|
||||
for page in self.stack.get_children():
|
||||
self.stack.remove(page)
|
||||
|
||||
def set_page(self, page, name):
|
||||
self.remove_all_pages()
|
||||
self.stack.add_named(page, name)
|
||||
page.update()
|
||||
page.show_all()
|
||||
self.stack.set_visible_child(page)
|
||||
|
||||
def update_proxy_list(self):
|
||||
page = self.stack.get_child_by_name('connetion')
|
||||
if page is None:
|
||||
return
|
||||
page.options['proxy'].update_values()
|
||||
|
||||
def check_relogin(self):
|
||||
for account in self.need_relogin:
|
||||
options = self.get_relogin_options(account)
|
||||
active = app.config.get_per('accounts', account, 'active')
|
||||
if options != self.need_relogin[account]:
|
||||
self.need_relogin[account] = options
|
||||
if active:
|
||||
self.relog(account)
|
||||
break
|
||||
|
||||
def relog(self, account):
|
||||
if app.connections[account].connected == 0:
|
||||
return
|
||||
|
||||
if account == app.ZEROCONF_ACC_NAME:
|
||||
app.connections[app.ZEROCONF_ACC_NAME].update_details()
|
||||
return
|
||||
|
||||
def login(account, show_before, status_before):
|
||||
"""
|
||||
Login with previous status
|
||||
"""
|
||||
# first make sure connection is really closed,
|
||||
# 0.5 may not be enough
|
||||
app.connections[account].disconnect(True)
|
||||
app.interface.roster.send_status(
|
||||
account, show_before, status_before)
|
||||
|
||||
def relog(account):
|
||||
show_before = app.SHOW_LIST[app.connections[account].connected]
|
||||
status_before = app.connections[account].status
|
||||
app.interface.roster.send_status(
|
||||
account, 'offline', _('Be right back.'))
|
||||
GLib.timeout_add(500, login, account, show_before, status_before)
|
||||
|
||||
dialogs.YesNoDialog(
|
||||
_('Relogin now?'),
|
||||
_('If you want all the changes to apply instantly, '
|
||||
'you must relogin.'),
|
||||
transient_for=self,
|
||||
on_response_yes=lambda *args: relog(account))
|
||||
|
||||
@staticmethod
|
||||
def get_relogin_options(account):
|
||||
if account == app.ZEROCONF_ACC_NAME:
|
||||
options = ['zeroconf_first_name', 'zeroconf_last_name',
|
||||
'zeroconf_jabber_id', 'zeroconf_email', 'keyid']
|
||||
else:
|
||||
options = ['client_cert', 'proxy', 'resource',
|
||||
'use_custom_host', 'custom_host', 'custom_port',
|
||||
'keyid']
|
||||
|
||||
values = []
|
||||
for option in options:
|
||||
values.append(app.config.get_per('accounts', account, option))
|
||||
return values
|
||||
|
||||
def on_remove_account(self, button, account):
|
||||
if app.events.get_events(account):
|
||||
dialogs.ErrorDialog(
|
||||
_('Unread events'),
|
||||
_('Read all pending events before removing this account.'),
|
||||
transient_for=self)
|
||||
return
|
||||
|
||||
if app.config.get_per('accounts', account, 'is_zeroconf'):
|
||||
# Should never happen as button is insensitive
|
||||
return
|
||||
|
||||
win_opened = False
|
||||
if app.interface.msg_win_mgr.get_controls(acct=account):
|
||||
win_opened = True
|
||||
elif account in app.interface.instances:
|
||||
for key in app.interface.instances[account]:
|
||||
if (app.interface.instances[account][key] and
|
||||
key != 'remove_account'):
|
||||
win_opened = True
|
||||
break
|
||||
|
||||
# Detect if we have opened windows for this account
|
||||
|
||||
def remove(account):
|
||||
if (account in app.interface.instances and
|
||||
'remove_account' in app.interface.instances[account]):
|
||||
dialog = app.interface.instances[account]['remove_account']
|
||||
dialog.window.present()
|
||||
else:
|
||||
if account not in app.interface.instances:
|
||||
app.interface.instances[account] = {}
|
||||
app.interface.instances[account]['remove_account'] = \
|
||||
config.RemoveAccountWindow(account)
|
||||
if win_opened:
|
||||
dialogs.ConfirmationDialog(
|
||||
_('You have opened chat in account %s') % account,
|
||||
_('All chat and groupchat windows will be closed. '
|
||||
'Do you want to continue?'),
|
||||
on_response_ok=(remove, account))
|
||||
else:
|
||||
remove(account)
|
||||
|
||||
def remove_account(self, account):
|
||||
for row in self.account_list.get_children():
|
||||
if row.get_child().account == account:
|
||||
self.account_list.remove(row)
|
||||
del self.need_relogin[account]
|
||||
break
|
||||
|
||||
def add_account(self, account):
|
||||
account_item = Account(account, self)
|
||||
self.account_list.add(account_item)
|
||||
account_item.set_activatable()
|
||||
self.account_list.show_all()
|
||||
self.stack.show_all()
|
||||
self.need_relogin[account] = self.get_relogin_options(account)
|
||||
|
||||
def select_account(self, account):
|
||||
for row in self.account_list.get_children():
|
||||
if row.get_child().account == account:
|
||||
self.account_list.emit('row-activated', row)
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def enable_account(account):
|
||||
if account == app.ZEROCONF_ACC_NAME:
|
||||
app.connections[account] = ConnectionZeroconf(account)
|
||||
else:
|
||||
app.connections[account] = Connection(account)
|
||||
|
||||
# update variables
|
||||
app.interface.instances[account] = {
|
||||
'infos': {}, 'disco': {}, 'gc_config': {}, 'search': {},
|
||||
'online_dialog': {}, 'sub_request': {}}
|
||||
app.interface.minimized_controls[account] = {}
|
||||
app.connections[account].connected = 0
|
||||
app.groups[account] = {}
|
||||
app.contacts.add_account(account)
|
||||
app.gc_connected[account] = {}
|
||||
app.automatic_rooms[account] = {}
|
||||
app.newly_added[account] = []
|
||||
app.to_be_removed[account] = []
|
||||
if account == app.ZEROCONF_ACC_NAME:
|
||||
app.nicks[account] = app.ZEROCONF_ACC_NAME
|
||||
else:
|
||||
app.nicks[account] = app.config.get_per(
|
||||
'accounts', account, 'name')
|
||||
app.block_signed_in_notifications[account] = True
|
||||
app.sleeper_state[account] = 'off'
|
||||
app.encrypted_chats[account] = []
|
||||
app.last_message_time[account] = {}
|
||||
app.status_before_autoaway[account] = ''
|
||||
app.transport_avatar[account] = {}
|
||||
app.gajim_optional_features[account] = []
|
||||
app.caps_hash[account] = ''
|
||||
helpers.update_optional_features(account)
|
||||
# refresh roster
|
||||
if len(app.connections) >= 2:
|
||||
# Do not merge accounts if only one exists
|
||||
app.interface.roster.regroup = app.config.get('mergeaccounts')
|
||||
else:
|
||||
app.interface.roster.regroup = False
|
||||
app.interface.roster.setup_and_draw_roster()
|
||||
gui_menu_builder.build_accounts_menu()
|
||||
|
||||
@staticmethod
|
||||
def disable_account(account):
|
||||
app.interface.roster.close_all(account)
|
||||
if account == app.ZEROCONF_ACC_NAME:
|
||||
app.connections[account].disable_account()
|
||||
app.connections[account].cleanup()
|
||||
del app.connections[account]
|
||||
del app.interface.instances[account]
|
||||
del app.interface.minimized_controls[account]
|
||||
del app.nicks[account]
|
||||
del app.block_signed_in_notifications[account]
|
||||
del app.groups[account]
|
||||
app.contacts.remove_account(account)
|
||||
del app.gc_connected[account]
|
||||
del app.automatic_rooms[account]
|
||||
del app.to_be_removed[account]
|
||||
del app.newly_added[account]
|
||||
del app.sleeper_state[account]
|
||||
del app.encrypted_chats[account]
|
||||
del app.last_message_time[account]
|
||||
del app.status_before_autoaway[account]
|
||||
del app.transport_avatar[account]
|
||||
del app.gajim_optional_features[account]
|
||||
del app.caps_hash[account]
|
||||
if len(app.connections) >= 2:
|
||||
# Do not merge accounts if only one exists
|
||||
app.interface.roster.regroup = app.config.get('mergeaccounts')
|
||||
else:
|
||||
app.interface.roster.regroup = False
|
||||
app.interface.roster.setup_and_draw_roster()
|
||||
gui_menu_builder.build_accounts_menu()
|
||||
|
||||
|
||||
class Account(Gtk.Box):
|
||||
def __init__(self, account, parent):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL,
|
||||
spacing=12)
|
||||
self.account = account
|
||||
if account == app.ZEROCONF_ACC_NAME:
|
||||
self.options = ZeroConfPage(account)
|
||||
else:
|
||||
self.options = AccountPage(account)
|
||||
self.parent = parent
|
||||
|
||||
switch = Gtk.Switch()
|
||||
switch.set_active(app.config.get_per('accounts', account, 'active'))
|
||||
switch.set_vexpand(False)
|
||||
switch.set_valign(Gtk.Align.CENTER)
|
||||
switch.set_halign(Gtk.Align.START)
|
||||
if account == app.ZEROCONF_ACC_NAME and not app.HAVE_ZEROCONF:
|
||||
switch.set_sensitive(False)
|
||||
switch.set_active(False)
|
||||
switch.connect('notify::active', self.on_switch, self.account)
|
||||
|
||||
account_label = app.config.get_per('accounts', account, 'account_label')
|
||||
self.label = Gtk.Label(label=account_label or account)
|
||||
self.label.set_halign(Gtk.Align.START)
|
||||
self.label.set_hexpand(True)
|
||||
|
||||
self.add(switch)
|
||||
self.add(self.label)
|
||||
|
||||
if account != app.ZEROCONF_ACC_NAME:
|
||||
button = get_image_button('list-remove-symbolic', 'Remove')
|
||||
button.connect('clicked', parent.on_remove_account, account)
|
||||
self.add(button)
|
||||
|
||||
def set_activatable(self):
|
||||
if self.account == app.ZEROCONF_ACC_NAME:
|
||||
self.get_parent().set_activatable(app.HAVE_ZEROCONF)
|
||||
|
||||
def on_switch(self, switch, param, account):
|
||||
old_state = app.config.get_per('accounts', account, 'active')
|
||||
state = switch.get_active()
|
||||
if old_state == state:
|
||||
return
|
||||
|
||||
if (account in app.connections and
|
||||
app.connections[account].connected > 0):
|
||||
# connecting or connected
|
||||
dialogs.ErrorDialog(
|
||||
_('You are currently connected to the server'),
|
||||
_('To disable the account, you must be disconnected.'),
|
||||
transient_for=self.parent)
|
||||
switch.set_active(not state)
|
||||
return
|
||||
if state:
|
||||
self.parent.enable_account(account)
|
||||
else:
|
||||
self.parent.disable_account(account)
|
||||
app.config.set_per('accounts', account, 'active', state)
|
||||
|
||||
def on_row_activated(self):
|
||||
self.options.update_states()
|
||||
self.parent.set_page(self.options, 'account')
|
||||
|
||||
def update(self):
|
||||
account_label = app.config.get_per(
|
||||
'accounts', self.account, 'account_label')
|
||||
self.label.set_text(account_label or self.account)
|
||||
|
||||
|
||||
class GenericOptionPage(Gtk.Box):
|
||||
def __init__(self, account, parent, options):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
self.account = account
|
||||
self.parent = parent
|
||||
|
||||
self.toggle = get_image_button('document-edit-symbolic',
|
||||
_('Rename account label'), toggle=True)
|
||||
self.toggle.connect('toggled', self.set_entry_text)
|
||||
|
||||
self.entry = Gtk.Entry()
|
||||
self.entry.set_sensitive(False)
|
||||
self.entry.set_name('AccountNameEntry')
|
||||
self.set_entry_text(self.toggle, update=True)
|
||||
|
||||
box = Gtk.Box()
|
||||
if isinstance(self, AccountPage):
|
||||
box.pack_start(self.toggle, False, True, 0)
|
||||
box.pack_start(self.entry, True, True, 0)
|
||||
|
||||
self.listbox = OptionsBox(account)
|
||||
self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
|
||||
for option in options:
|
||||
self.listbox.add_option(option)
|
||||
self.listbox.update_states()
|
||||
|
||||
self.pack_start(box, False, False, 0)
|
||||
self.pack_start(self.listbox, True, True, 0)
|
||||
|
||||
self.listbox.connect('row-activated', self.on_row_activated)
|
||||
|
||||
def update_states(self):
|
||||
self.listbox.update_states()
|
||||
|
||||
def on_row_activated(self, listbox, row):
|
||||
self.toggle.set_active(False)
|
||||
row.get_child().on_row_activated()
|
||||
|
||||
def set_entry_text(self, toggle, update=False):
|
||||
account_label = app.config.get_per(
|
||||
'accounts', self.account, 'account_label')
|
||||
if update:
|
||||
self.entry.set_text(account_label or self.account)
|
||||
return
|
||||
if toggle.get_active():
|
||||
self.entry.set_sensitive(True)
|
||||
self.entry.grab_focus()
|
||||
else:
|
||||
self.entry.set_sensitive(False)
|
||||
value = self.entry.get_text()
|
||||
if not value:
|
||||
value = account_label or self.account
|
||||
app.config.set_per('accounts', self.account,
|
||||
'account_label', value or self.account)
|
||||
if app.config.get_per('accounts', self.account, 'active'):
|
||||
app.interface.roster.draw_account(self.account)
|
||||
|
||||
def update(self):
|
||||
self.set_entry_text(self.toggle, update=True)
|
||||
|
||||
def set_page(self, options, name):
|
||||
options.update_states()
|
||||
self.get_toplevel().set_page(options, name)
|
||||
|
||||
|
||||
class AccountPage(GenericOptionPage):
|
||||
def __init__(self, account, parent=None):
|
||||
|
||||
general = partial(
|
||||
self.set_page, GeneralPage(account, self), 'general')
|
||||
connection = partial(
|
||||
self.set_page, ConnectionPage(account, self), 'connection')
|
||||
|
||||
options = [
|
||||
Option(OptionKind.LOGIN, _('Login'), OptionType.DIALOG,
|
||||
props={'dialog': LoginDialog}),
|
||||
|
||||
Option(OptionKind.ACTION, _('Profile'), OptionType.ACTION,
|
||||
'-profile', props={'action_args': account}),
|
||||
|
||||
Option(OptionKind.CALLBACK, _('General'),
|
||||
name='general', props={'callback': general}),
|
||||
|
||||
Option(OptionKind.CALLBACK, _('Connection'),
|
||||
name='connection', props={'callback': connection}),
|
||||
|
||||
Option(OptionKind.ACTION, _('Import Contacts'), OptionType.ACTION,
|
||||
'-import-contacts', props={'action_args': account}),
|
||||
|
||||
Option(OptionKind.DIALOG, _('Client Certificate'),
|
||||
OptionType.DIALOG, props={'dialog': CertificateDialog}),
|
||||
|
||||
Option(OptionKind.GPG, _('OpenPGP Key'), OptionType.DIALOG,
|
||||
props={'dialog': None}),
|
||||
]
|
||||
|
||||
GenericOptionPage.__init__(self, account, parent, options)
|
||||
|
||||
|
||||
class GeneralPage(GenericOptionPage):
|
||||
def __init__(self, account, parent=None):
|
||||
|
||||
options = [
|
||||
Option(OptionKind.SWITCH, _('Connect on startup'),
|
||||
OptionType.ACCOUNT_CONFIG, 'autoconnect'),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Reconnect when connection is lost'),
|
||||
OptionType.ACCOUNT_CONFIG, 'autoreconnect'),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Save conversations for all contacts'),
|
||||
OptionType.ACCOUNT_CONFIG, 'no_log_for',
|
||||
desc=_('Store conversations on the harddrive')),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Server Message Archive'),
|
||||
OptionType.ACCOUNT_CONFIG, 'sync_logs_with_server',
|
||||
desc=_('Messages get stored on the server.\n'
|
||||
'The archive is used to sync messages\n'
|
||||
'between multiple devices.\n'
|
||||
'XEP-0313')),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Global Status'),
|
||||
OptionType.ACCOUNT_CONFIG, 'sync_with_global_status',
|
||||
desc=_('Synchronise the status of all accounts')),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Message Carbons'),
|
||||
OptionType.ACCOUNT_CONFIG, 'enable_message_carbons',
|
||||
desc=_('All your other online devices get copies\n'
|
||||
'of sent and received messages.\n'
|
||||
'XEP-0280')),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Use file transfer proxies'),
|
||||
OptionType.ACCOUNT_CONFIG, 'use_ft_proxies'),
|
||||
]
|
||||
GenericOptionPage.__init__(self, account, parent, options)
|
||||
|
||||
|
||||
class ConnectionPage(GenericOptionPage):
|
||||
def __init__(self, account, parent=None):
|
||||
|
||||
options = [
|
||||
Option(OptionKind.SWITCH, 'HTTP_PROXY',
|
||||
OptionType.ACCOUNT_CONFIG, 'use_env_http_proxy',
|
||||
desc=_('Use environment variable')),
|
||||
|
||||
Option(OptionKind.PROXY, _('Proxy'),
|
||||
OptionType.ACCOUNT_CONFIG, 'proxy', name='proxy'),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Warn on insecure connection'),
|
||||
OptionType.ACCOUNT_CONFIG,
|
||||
'warn_when_insecure_ssl_connection'),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Send keep-alive packets'),
|
||||
OptionType.ACCOUNT_CONFIG, 'keep_alives_enabled'),
|
||||
|
||||
Option(OptionKind.HOSTNAME, _('Hostname'), OptionType.DIALOG,
|
||||
desc=_('Manually set the hostname for the server'),
|
||||
props={'dialog': CutstomHostnameDialog}),
|
||||
|
||||
Option(OptionKind.ENTRY, _('Resource'),
|
||||
OptionType.ACCOUNT_CONFIG, 'resource'),
|
||||
|
||||
Option(OptionKind.PRIORITY, _('Priority'),
|
||||
OptionType.DIALOG, props={'dialog': PriorityDialog}),
|
||||
]
|
||||
|
||||
GenericOptionPage.__init__(self, account, parent, options)
|
||||
|
||||
|
||||
class ZeroConfPage(GenericOptionPage):
|
||||
def __init__(self, account, parent=None):
|
||||
|
||||
options = [
|
||||
Option(OptionKind.DIALOG, _('Credentials'),
|
||||
OptionType.DIALOG, props={'dialog': CredentialsDialog}),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Connect on startup'),
|
||||
OptionType.ACCOUNT_CONFIG, 'autoconnect',
|
||||
desc=_('Use environment variable')),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Save conversations for all contacts'),
|
||||
OptionType.ACCOUNT_CONFIG, 'no_log_for',
|
||||
desc=_('Store conversations on the harddrive')),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Global Status'),
|
||||
OptionType.ACCOUNT_CONFIG, 'sync_with_global_status',
|
||||
desc=_('Synchronize the status of all accounts')),
|
||||
|
||||
Option(OptionKind.GPG, _('OpenPGP Key'),
|
||||
OptionType.DIALOG, props={'dialog': None}),
|
||||
]
|
||||
|
||||
GenericOptionPage.__init__(self, account, parent, options)
|
||||
|
||||
|
||||
class CredentialsDialog(OptionsDialog):
|
||||
def __init__(self, account, parent):
|
||||
|
||||
options = [
|
||||
Option(OptionKind.ENTRY, _('First Name'),
|
||||
OptionType.ACCOUNT_CONFIG, 'zeroconf_first_name'),
|
||||
|
||||
Option(OptionKind.ENTRY, _('Last Name'),
|
||||
OptionType.ACCOUNT_CONFIG, 'zeroconf_last_name'),
|
||||
|
||||
Option(OptionKind.ENTRY, _('Jabber ID'),
|
||||
OptionType.ACCOUNT_CONFIG, 'zeroconf_jabber_id'),
|
||||
|
||||
Option(OptionKind.ENTRY, _('Email'),
|
||||
OptionType.ACCOUNT_CONFIG, 'zeroconf_email'),
|
||||
]
|
||||
|
||||
OptionsDialog.__init__(self, parent, _('Credential Options'),
|
||||
Gtk.DialogFlags.MODAL, options, account)
|
||||
|
||||
|
||||
class PriorityDialog(OptionsDialog):
|
||||
def __init__(self, account, parent):
|
||||
|
||||
neg_priority = app.config.get('enable_negative_priority')
|
||||
if neg_priority:
|
||||
range_ = (-128, 127)
|
||||
else:
|
||||
range_ = (0, 127)
|
||||
|
||||
options = [
|
||||
Option(OptionKind.SWITCH, _('Adjust to status'),
|
||||
OptionType.ACCOUNT_CONFIG, 'adjust_priority_with_status',
|
||||
'adjust'),
|
||||
|
||||
Option(OptionKind.SPIN, _('Priority'),
|
||||
OptionType.ACCOUNT_CONFIG, 'priority',
|
||||
enabledif=('adjust', False), props={'range_': range_}),
|
||||
]
|
||||
|
||||
OptionsDialog.__init__(self, parent, _('Priority'),
|
||||
Gtk.DialogFlags.MODAL, options, account)
|
||||
|
||||
self.connect('destroy', self.on_destroy)
|
||||
|
||||
def on_destroy(self, *args):
|
||||
# Update priority
|
||||
if self.account not in app.connections:
|
||||
return
|
||||
show = app.SHOW_LIST[app.connections[self.account].connected]
|
||||
status = app.connections[self.account].status
|
||||
app.connections[self.account].change_status(show, status)
|
||||
|
||||
|
||||
class CutstomHostnameDialog(OptionsDialog):
|
||||
def __init__(self, account, parent):
|
||||
|
||||
options = [
|
||||
Option(OptionKind.SWITCH, _('Enable'),
|
||||
OptionType.ACCOUNT_CONFIG, 'use_custom_host', name='custom'),
|
||||
|
||||
Option(OptionKind.ENTRY, _('Hostname'),
|
||||
OptionType.ACCOUNT_CONFIG, 'custom_host',
|
||||
enabledif=('custom', True)),
|
||||
|
||||
Option(OptionKind.ENTRY, _('Port'),
|
||||
OptionType.ACCOUNT_CONFIG, 'custom_port',
|
||||
enabledif=('custom', True)),
|
||||
]
|
||||
|
||||
OptionsDialog.__init__(self, parent, _('Connection Options'),
|
||||
Gtk.DialogFlags.MODAL, options, account)
|
||||
|
||||
|
||||
class CertificateDialog(OptionsDialog):
|
||||
def __init__(self, account, parent):
|
||||
|
||||
options = [
|
||||
Option(OptionKind.FILECHOOSER, _('Client Certificate'),
|
||||
OptionType.ACCOUNT_CONFIG, 'client_cert',
|
||||
props={'filefilter': (_('PKCS12 Files'), '*.p12')}),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Encrypted Certificate'),
|
||||
OptionType.ACCOUNT_CONFIG, 'client_cert_encrypted'),
|
||||
]
|
||||
|
||||
OptionsDialog.__init__(self, parent, _('Certificate Options'),
|
||||
Gtk.DialogFlags.MODAL, options, account)
|
||||
|
||||
|
||||
class LoginDialog(OptionsDialog):
|
||||
def __init__(self, account, parent):
|
||||
|
||||
options = [
|
||||
Option(OptionKind.ENTRY, _('Password'),
|
||||
OptionType.ACCOUNT_CONFIG, 'password', name='password',
|
||||
enabledif=('savepass', True)),
|
||||
|
||||
Option(OptionKind.SWITCH, _('Save Password'),
|
||||
OptionType.ACCOUNT_CONFIG, 'savepass', name='savepass'),
|
||||
|
||||
Option(OptionKind.CHANGEPASSWORD, _('Change Password'),
|
||||
OptionType.DIALOG, callback=self.on_password_change,
|
||||
props={'dialog': None}),
|
||||
]
|
||||
|
||||
OptionsDialog.__init__(self, parent, _('Login Options'),
|
||||
Gtk.DialogFlags.MODAL, options, account)
|
||||
|
||||
self.connect('destroy', self.on_destroy)
|
||||
|
||||
def on_password_change(self, new_password, data):
|
||||
self.get_option('password').entry.set_text(new_password)
|
||||
|
||||
def on_destroy(self, *args):
|
||||
savepass = app.config.get_per('accounts', self.account, 'savepass')
|
||||
if not savepass:
|
||||
passwords.save_password(self.account, '')
|
|
@ -25,10 +25,12 @@ from gajim.common.exceptions import GajimGeneralException
|
|||
from gi.repository import Gtk
|
||||
import sys
|
||||
import os
|
||||
|
||||
from gajim import config
|
||||
from gajim import dialogs
|
||||
from gajim import features_window
|
||||
from gajim import shortcuts_window
|
||||
from gajim import accounts_window
|
||||
import gajim.plugins.gui
|
||||
from gajim import history_window
|
||||
from gajim import disco
|
||||
|
@ -57,10 +59,10 @@ class AppActions():
|
|||
interface.instances['plugins'] = gajim.plugins.gui.PluginsWindow()
|
||||
|
||||
def on_accounts(self, action, param):
|
||||
if 'accounts' in interface.instances:
|
||||
interface.instances['accounts'].window.present()
|
||||
if 'accounts' in app.interface.instances:
|
||||
app.interface.instances['accounts'].present()
|
||||
else:
|
||||
interface.instances['accounts'] = config.AccountsWindow()
|
||||
app.interface.instances['accounts'] = accounts_window.AccountsWindow()
|
||||
|
||||
def on_history_manager(self, action, param):
|
||||
from gajim.history_manager import HistoryManager
|
||||
|
@ -131,6 +133,35 @@ class AppActions():
|
|||
def on_single_message(self, action, param):
|
||||
dialogs.SingleMessageWindow(param.get_string(), action='send')
|
||||
|
||||
def on_merge_accounts(self, action, param):
|
||||
action.set_state(param)
|
||||
value = param.get_boolean()
|
||||
app.config.set('mergeaccounts', value)
|
||||
if len(app.connections) >= 2: # Do not merge accounts if only one active
|
||||
app.interface.roster.regroup = value
|
||||
else:
|
||||
app.interface.roster.regroup = False
|
||||
app.interface.roster.setup_and_draw_roster()
|
||||
|
||||
def on_use_pgp_agent(self, action, param):
|
||||
action.set_state(param)
|
||||
app.config.set('use_gpg_agent', param.get_boolean())
|
||||
|
||||
def on_add_account(self, action, param):
|
||||
if 'account_creation_wizard' in app.interface.instances:
|
||||
app.interface.instances['account_creation_wizard'].window.present()
|
||||
else:
|
||||
app.interface.instances['account_creation_wizard'] = \
|
||||
config.AccountCreationWizardWindow()
|
||||
|
||||
def on_import_contacts(self, action, param):
|
||||
account = param.get_string()
|
||||
if 'import_contacts' in app.interface.instances:
|
||||
app.interface.instances['import_contacts'].dialog.present()
|
||||
else:
|
||||
app.interface.instances['import_contacts'] = \
|
||||
dialogs.SynchroniseSelectAccountDialog(account)
|
||||
|
||||
# Advanced Actions
|
||||
|
||||
def on_archiving_preferences(self, action, param):
|
||||
|
@ -174,6 +205,13 @@ class AppActions():
|
|||
interface.instances[account]['xml_console'] = \
|
||||
dialogs.XMLConsoleWindow(account)
|
||||
|
||||
def on_manage_proxies(self, action, param):
|
||||
if 'manage_proxies' in app.interface.instances:
|
||||
app.interface.instances['manage_proxies'].window.present()
|
||||
else:
|
||||
app.interface.instances['manage_proxies'] = \
|
||||
config.ManageProxiesWindow(interface.roster.window)
|
||||
|
||||
# Admin Actions
|
||||
|
||||
def on_set_motd(self, action, param):
|
||||
|
|
|
@ -298,7 +298,8 @@ class StandardGroupChatCommands(CommandContainer):
|
|||
@doc(_("Invite a user to a room for a reason"))
|
||||
def invite(self, jid, reason):
|
||||
self.connection.send_invite(self.room_jid, jid, reason)
|
||||
return _("Invited %s to %s") % (jid, self.room_jid)
|
||||
return _("Invited %(jid)s to %(room_jid)s") % {'jid': jid,
|
||||
'room_jid': self.room_jid}
|
||||
|
||||
@command(raw=True, empty=True)
|
||||
@doc(_("Join a group chat given by a jid, optionally using given nickname"))
|
||||
|
|
|
@ -53,7 +53,7 @@ ged = ged_module.GlobalEventsDispatcher() # Global Events Dispatcher
|
|||
nec = None # Network Events Controller
|
||||
plugin_manager = None # Plugins Manager
|
||||
|
||||
log = logging.getLogger('gajim')
|
||||
glog = logging.getLogger('gajim')
|
||||
|
||||
logger = None
|
||||
|
||||
|
@ -88,8 +88,6 @@ else:
|
|||
|
||||
os_info = None # used to cache os information
|
||||
|
||||
gmail_domains = ['gmail.com', 'googlemail.com']
|
||||
|
||||
transport_type = {} # list the type of transport
|
||||
|
||||
last_message_time = {} # list of time of the latest incomming message
|
||||
|
@ -176,7 +174,7 @@ try:
|
|||
'''
|
||||
v_gnupg = gnupg.__version__
|
||||
if V(v_gnupg) < V('0.3.8') or V(v_gnupg) > V('1.0.0'):
|
||||
log.info('Gajim needs python-gnupg >= 0.3.8')
|
||||
glog.info('Gajim needs python-gnupg >= 0.3.8')
|
||||
HAVE_GPG = False
|
||||
except ImportError:
|
||||
HAVE_GPG = False
|
||||
|
@ -247,7 +245,7 @@ try:
|
|||
if sleepy.SUPPORTED:
|
||||
HAVE_IDLE = True
|
||||
except Exception:
|
||||
log.debug(_('Unable to load idle module'))
|
||||
glog.info(_('Unable to load idle module'))
|
||||
HAVE_IDLE = False
|
||||
|
||||
|
||||
|
@ -261,7 +259,8 @@ gajim_common_features = [nbxmpp.NS_BYTESTREAM, nbxmpp.NS_SI, nbxmpp.NS_FILE,
|
|||
nbxmpp.NS_SSN, nbxmpp.NS_MOOD, nbxmpp.NS_ACTIVITY, nbxmpp.NS_NICK,
|
||||
nbxmpp.NS_ROSTERX, nbxmpp.NS_SECLABEL, nbxmpp.NS_HASHES_2,
|
||||
nbxmpp.NS_HASHES_MD5, nbxmpp.NS_HASHES_SHA1, nbxmpp.NS_HASHES_SHA256,
|
||||
nbxmpp.NS_HASHES_SHA512, nbxmpp.NS_CONFERENCE, nbxmpp.NS_CORRECT]
|
||||
nbxmpp.NS_HASHES_SHA512, nbxmpp.NS_CONFERENCE, nbxmpp.NS_CORRECT,
|
||||
nbxmpp.NS_EME]
|
||||
|
||||
# Optional features gajim supports per account
|
||||
gajim_optional_features = {}
|
||||
|
@ -493,3 +492,7 @@ def get_priority(account, show):
|
|||
elif prio > 127:
|
||||
prio = 127
|
||||
return prio
|
||||
|
||||
def log(domain):
|
||||
root = 'gajim.'
|
||||
return logging.getLogger(root + domain)
|
||||
|
|
|
@ -250,7 +250,7 @@ def check_and_possibly_move_config():
|
|||
continue
|
||||
if not os.path.exists(src):
|
||||
continue
|
||||
print(_('moving %s to %s') % (src, dst))
|
||||
print(_('moving %(src)s to %(dst)s') % {'src': src, 'dst': dst})
|
||||
shutil.move(src, dst)
|
||||
app.logger.init_vars()
|
||||
app.logger.attach_cache_database()
|
||||
|
|
|
@ -123,6 +123,7 @@ class Config:
|
|||
'print_time': [ opt_str, 'always', _('\'always\' - print time for every message.\n\'sometimes\' - print time every print_ichat_every_foo_minutes minute.\n\'never\' - never print time.')],
|
||||
'print_time_fuzzy': [ opt_int, 0, _('Print time in chats using Fuzzy Clock. Value of fuzziness from 1 to 4, or 0 to disable fuzzyclock. 1 is the most precise clock, 4 the least precise one. This is used only if print_time is \'sometimes\'.') ],
|
||||
'emoticons_theme': [opt_str, 'noto-emoticons', '', True ],
|
||||
'ascii_emoticons': [opt_bool, True, _('Enable ASCII emoticons'), True],
|
||||
'ascii_formatting': [ opt_bool, True,
|
||||
_('Treat * / _ pairs as possible formatting characters.'), True],
|
||||
'show_ascii_formatting_chars': [ opt_bool, True, _('If True, do not '
|
||||
|
@ -176,9 +177,6 @@ class Config:
|
|||
'time_stamp': [ opt_str, '[%X] ', _('This option let you customize timestamp that is printed in conversation. For exemple "[%H:%M] " will show "[hour:minute] ". See python doc on strftime for full documentation: http://docs.python.org/lib/module-time.html') ],
|
||||
'before_nickname': [ opt_str, '', _('Characters that are printed before the nickname in conversations') ],
|
||||
'after_nickname': [ opt_str, ':', _('Characters that are printed after the nickname in conversations') ],
|
||||
'notify_on_new_gmail_email': [ opt_bool, True ],
|
||||
'notify_on_new_gmail_email_extra': [ opt_bool, False ],
|
||||
'notify_on_new_gmail_email_command': [ opt_str, '', _('Specify the command to run when new mail arrives, e.g.: /usr/bin/getmail -q') ],
|
||||
'use_gpg_agent': [ opt_bool, False ],
|
||||
'change_roster_title': [ opt_bool, True, _('Add * and [n] in roster title?')],
|
||||
'restore_lines': [opt_int, 10, _('How many history messages should be restored when a chat tab/window is reopened?')],
|
||||
|
@ -192,7 +190,7 @@ class Config:
|
|||
'key_up_lines': [opt_int, 25, _('How many lines to store for Ctrl+KeyUP.')],
|
||||
'version': [ opt_str, defs.version ], # which version created the config
|
||||
'search_engine': [opt_str, 'https://www.google.com/search?&q=%s&sourceid=gajim'],
|
||||
'dictionary_url': [opt_str, 'WIKTIONARY', _("Either custom url with %s in it where %s is the word/phrase or 'WIKTIONARY' which means use wiktionary.")],
|
||||
'dictionary_url': [opt_str, 'WIKTIONARY', _("Either custom url with %%s in it where %%s is the word/phrase or 'WIKTIONARY' which means use wiktionary.")],
|
||||
'always_english_wikipedia': [opt_bool, False],
|
||||
'always_english_wiktionary': [opt_bool, True],
|
||||
'remote_control': [opt_bool, False, _('If checked, Gajim can be controlled remotely using gajim-remote.'), True],
|
||||
|
@ -323,6 +321,7 @@ class Config:
|
|||
__options_per_key = {
|
||||
'accounts': ({
|
||||
'name': [ opt_str, '', '', True ],
|
||||
'account_label': [ opt_str, '', '', False ],
|
||||
'hostname': [ opt_str, '', '', True ],
|
||||
'anonymous_auth': [ opt_bool, False ],
|
||||
'client_cert': [ opt_str, '', '', True ],
|
||||
|
@ -535,7 +534,6 @@ class Config:
|
|||
'message_sent': [ False, 'sent.wav' ],
|
||||
'muc_message_highlight': [ True, 'gc_message1.wav', _('Sound to play when a group chat message contains one of the words in muc_highlight_words, or when a group chat message contains your nickname.')],
|
||||
'muc_message_received': [ False, 'gc_message2.wav', _('Sound to play when any MUC message arrives.') ],
|
||||
'gmail_received': [ False, 'message1.wav' ],
|
||||
}
|
||||
|
||||
themes_default = {
|
||||
|
|
|
@ -98,9 +98,11 @@ class ConfigPaths:
|
|||
base = expand('~/.local/share')
|
||||
self.data_root = os.path.join(base, 'gajim')
|
||||
|
||||
basedir = os.environ.get('GAJIM_BASEDIR', defs.basedir)
|
||||
import pkg_resources
|
||||
basedir = pkg_resources.resource_filename("gajim", ".")
|
||||
self.add('DATA', None, os.path.join(basedir, 'data'))
|
||||
self.add('GUI', None, os.path.join(basedir, 'data', 'gui'))
|
||||
basedir = os.environ.get('GAJIM_BASEDIR', defs.basedir)
|
||||
self.add('ICONS', None, os.path.join(basedir, 'icons'))
|
||||
self.add('HOME', None, os.path.expanduser('~'))
|
||||
self.add('PLUGINS_BASE', None, os.path.join(basedir, 'plugins'))
|
||||
|
@ -142,8 +144,7 @@ class ConfigPaths:
|
|||
d = {'LOG_DB': 'logs.db', 'MY_CACERTS': 'cacerts.pem',
|
||||
'MY_EMOTS': 'emoticons', 'MY_ICONSETS': 'iconsets',
|
||||
'MY_MOOD_ICONSETS': 'moods', 'MY_ACTIVITY_ICONSETS': 'activities',
|
||||
'PLUGINS_USER': 'plugins',
|
||||
'RNG_SEED': 'rng_seed'}
|
||||
'PLUGINS_USER': 'plugins'}
|
||||
for name in d:
|
||||
d[name] += profile
|
||||
self.add(name, Type.DATA, windowsify(d[name]))
|
||||
|
|
|
@ -1083,7 +1083,8 @@ class Connection(CommonConnection, ConnectionHandlers):
|
|||
self._hosts = [ {'host': h, 'port': p, 'ssl_port': ssl_p, 'prio': 10,
|
||||
'weight': 10} ]
|
||||
self._hostname = hostname
|
||||
if use_srv:
|
||||
|
||||
if use_srv and self._proxy is None:
|
||||
# add request for srv query to the resolve, on result '_on_resolve'
|
||||
# will be called
|
||||
app.resolver.resolve('_xmpp-client._tcp.' + helpers.idn_to_ascii(
|
||||
|
@ -1451,7 +1452,9 @@ class Connection(CommonConnection, ConnectionHandlers):
|
|||
return
|
||||
if hasattr(con, 'Resource'):
|
||||
self.server_resource = con.Resource
|
||||
self.registered_name = con._registered_name
|
||||
if con._registered_name is not None:
|
||||
log.info('Bound JID: %s', con._registered_name)
|
||||
self.registered_name = con._registered_name
|
||||
if app.config.get_per('accounts', self.name, 'anonymous_auth'):
|
||||
# Get jid given by server
|
||||
old_jid = app.get_jid_from_account(self.name)
|
||||
|
@ -1821,9 +1824,10 @@ class Connection(CommonConnection, ConnectionHandlers):
|
|||
|
||||
self.sm.resuming = False # back to previous state
|
||||
# Discover Stun server(s)
|
||||
hostname = app.config.get_per('accounts', self.name, 'hostname')
|
||||
app.resolver.resolve('_stun._udp.' + helpers.idn_to_ascii(hostname),
|
||||
self._on_stun_resolved)
|
||||
if self._proxy is None:
|
||||
hostname = app.config.get_per('accounts', self.name, 'hostname')
|
||||
app.resolver.resolve('_stun._udp.' + helpers.idn_to_ascii(hostname),
|
||||
self._on_stun_resolved)
|
||||
|
||||
def _on_stun_resolved(self, host, result_array):
|
||||
if len(result_array) != 0:
|
||||
|
@ -1911,9 +1915,6 @@ class Connection(CommonConnection, ConnectionHandlers):
|
|||
get_action(self.name + '-archive').set_enabled(True)
|
||||
|
||||
if obj.fjid == hostname:
|
||||
if nbxmpp.NS_GMAILNOTIFY in obj.features:
|
||||
app.gmail_domains.append(obj.fjid)
|
||||
self.request_gmail_notifications()
|
||||
if nbxmpp.NS_SECLABEL in obj.features:
|
||||
self.seclabel_supported = True
|
||||
for identity in obj.identities:
|
||||
|
@ -2074,8 +2075,9 @@ class Connection(CommonConnection, ConnectionHandlers):
|
|||
return
|
||||
if type_ == 'message':
|
||||
if len(contacts) == 1:
|
||||
msg = _('Sent contact: "%s" (%s)') % (contacts[0].get_full_jid(),
|
||||
contacts[0].get_shown_name())
|
||||
msg = _('Sent contact: "%(jid)s" (%(name)s)') % {
|
||||
'jid': contacts[0].get_full_jid(),
|
||||
'name': contacts[0].get_shown_name()}
|
||||
else:
|
||||
msg = _('Sent contacts:')
|
||||
for contact in contacts:
|
||||
|
|
|
@ -1080,6 +1080,27 @@ class ConnectionHandlersBase:
|
|||
app.plugin_manager.extension_point(
|
||||
'decrypt', self, obj, self._on_message_received)
|
||||
if not obj.encrypted:
|
||||
# XEP-0380
|
||||
enc_tag = obj.stanza.getTag('encryption', namespace=nbxmpp.NS_EME)
|
||||
if enc_tag:
|
||||
ns = enc_tag.getAttr('namespace')
|
||||
if ns:
|
||||
if ns == 'urn:xmpp:otr:0':
|
||||
obj.msgtxt = _('This message was encrypted with OTR '
|
||||
'and could not be decrypted.')
|
||||
elif ns == 'jabber:x:encrypted':
|
||||
obj.msgtxt = _('This message was encrypted with Legacy '
|
||||
'OpenPGP and could not be decrypted. You can install '
|
||||
'the PGP plugin to handle those messages.')
|
||||
elif ns == 'urn:xmpp:openpgp:0':
|
||||
obj.msgtxt = _('This message was encrypted with '
|
||||
'OpenPGP for XMPP and could not be decrypted.')
|
||||
else:
|
||||
enc_name = enc_tag.getAttr('name')
|
||||
if not enc_name:
|
||||
enc_name = ns
|
||||
obj.msgtxt = _('This message was encrypted with %s '
|
||||
'and could not be decrypted.') % enc_name
|
||||
self._on_message_received(obj)
|
||||
|
||||
def _on_message_received(self, obj):
|
||||
|
@ -1155,6 +1176,8 @@ class ConnectionHandlersBase:
|
|||
return True
|
||||
|
||||
def _nec_gc_message_received(self, obj):
|
||||
if obj.conn.name != self.name:
|
||||
return
|
||||
if app.config.should_log(obj.conn.name, obj.jid) and not \
|
||||
obj.timestamp < obj.conn.last_history_time[obj.jid] and obj.msgtxt and \
|
||||
obj.nick:
|
||||
|
@ -1356,7 +1379,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
|
|||
client_caps_factory=capscache.create_suitable_client_caps)
|
||||
ConnectionJingle.__init__(self)
|
||||
ConnectionHandlersBase.__init__(self)
|
||||
self.gmail_url = None
|
||||
|
||||
# keep the latest subscribed event for each jid to prevent loop when we
|
||||
# acknowledge presences
|
||||
|
@ -1375,9 +1397,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
|
|||
|
||||
self.privacy_default_list = None
|
||||
|
||||
self.gmail_last_tid = None
|
||||
self.gmail_last_time = None
|
||||
|
||||
app.nec.register_incoming_event(PrivateStorageBookmarksReceivedEvent)
|
||||
app.nec.register_incoming_event(BookmarksReceivedEvent)
|
||||
app.nec.register_incoming_event(
|
||||
|
@ -1415,8 +1434,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
|
|||
self._nec_roster_received)
|
||||
app.ged.register_event_handler('iq-error-received', ged.CORE,
|
||||
self._nec_iq_error_received)
|
||||
app.ged.register_event_handler('gmail-new-mail-received', ged.CORE,
|
||||
self._nec_gmail_new_mail_received)
|
||||
app.ged.register_event_handler('ping-received', ged.CORE,
|
||||
self._nec_ping_received)
|
||||
app.ged.register_event_handler('subscribe-presence-received',
|
||||
|
@ -1461,8 +1478,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
|
|||
self._nec_roster_received)
|
||||
app.ged.remove_event_handler('iq-error-received', ged.CORE,
|
||||
self._nec_iq_error_received)
|
||||
app.ged.remove_event_handler('gmail-new-mail-received', ged.CORE,
|
||||
self._nec_gmail_new_mail_received)
|
||||
app.ged.remove_event_handler('ping-received', ged.CORE,
|
||||
self._nec_ping_received)
|
||||
app.ged.remove_event_handler('subscribe-presence-received',
|
||||
|
@ -1719,44 +1734,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
|
|||
app.nec.push_incoming_event(TimeResultReceivedEvent(None, conn=self,
|
||||
stanza=iq_obj))
|
||||
|
||||
def _gMailNewMailCB(self, con, iq_obj):
|
||||
"""
|
||||
Called when we get notified of new mail messages in gmail account
|
||||
"""
|
||||
log.debug('gMailNewMailCB')
|
||||
app.nec.push_incoming_event(GmailNewMailReceivedEvent(None, conn=self,
|
||||
stanza=iq_obj))
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _nec_gmail_new_mail_received(self, obj):
|
||||
if obj.conn.name != self.name:
|
||||
return
|
||||
if not self.connection or self.connected < 2:
|
||||
return
|
||||
# we'll now ask the server for the exact number of new messages
|
||||
jid = app.get_jid_from_account(self.name)
|
||||
log.debug('Got notification of new gmail e-mail on %s. Asking the '
|
||||
'server for more info.' % jid)
|
||||
iq = nbxmpp.Iq(typ='get')
|
||||
query = iq.setTag('query')
|
||||
query.setNamespace(nbxmpp.NS_GMAILNOTIFY)
|
||||
# we want only be notified about newer mails
|
||||
if self.gmail_last_tid:
|
||||
query.setAttr('newer-than-tid', self.gmail_last_tid)
|
||||
if self.gmail_last_time:
|
||||
query.setAttr('newer-than-time', self.gmail_last_time)
|
||||
self.connection.send(iq)
|
||||
|
||||
def _gMailQueryCB(self, con, iq_obj):
|
||||
"""
|
||||
Called when we receive results from Querying the server for mail messages
|
||||
in gmail account
|
||||
"""
|
||||
log.debug('gMailQueryCB')
|
||||
app.nec.push_incoming_event(GMailQueryReceivedEvent(None, conn=self,
|
||||
stanza=iq_obj))
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _rosterItemExchangeCB(self, con, msg):
|
||||
"""
|
||||
XEP-0144 Roster Item Echange
|
||||
|
@ -2121,28 +2098,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
|
|||
# hashes of already received messages
|
||||
self.received_message_hashes = []
|
||||
|
||||
def request_gmail_notifications(self):
|
||||
if not self.connection or self.connected < 2:
|
||||
return
|
||||
# It's a gmail account,
|
||||
# inform the server that we want e-mail notifications
|
||||
our_jid = helpers.parse_jid(app.get_jid_from_account(self.name))
|
||||
log.debug(('%s is a gmail account. Setting option '
|
||||
'to get e-mail notifications on the server.') % (our_jid))
|
||||
iq = nbxmpp.Iq(typ='set', to=our_jid)
|
||||
iq.setAttr('id', 'MailNotify')
|
||||
query = iq.setTag('usersetting')
|
||||
query.setNamespace(nbxmpp.NS_GTALKSETTING)
|
||||
query = query.setTag('mailnotifications')
|
||||
query.setAttr('value', 'true')
|
||||
self.connection.send(iq)
|
||||
# Ask how many messages there are now
|
||||
iq = nbxmpp.Iq(typ='get')
|
||||
iq.setID(self.connection.getAnID())
|
||||
query = iq.setTag('query')
|
||||
query.setNamespace(nbxmpp.NS_GMAILNOTIFY)
|
||||
self.connection.send(iq)
|
||||
|
||||
def _SearchCB(self, con, iq_obj):
|
||||
log.debug('SearchCB')
|
||||
app.nec.push_incoming_event(SearchFormReceivedEvent(None,
|
||||
|
@ -2258,10 +2213,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
|
|||
con.RegisterHandler('iq', self._HttpAuthCB, 'get', nbxmpp.NS_HTTP_AUTH)
|
||||
con.RegisterHandler('iq', self._CommandExecuteCB, 'set',
|
||||
nbxmpp.NS_COMMANDS)
|
||||
con.RegisterHandler('iq', self._gMailNewMailCB, 'set',
|
||||
nbxmpp.NS_GMAILNOTIFY)
|
||||
con.RegisterHandler('iq', self._gMailQueryCB, 'result',
|
||||
nbxmpp.NS_GMAILNOTIFY)
|
||||
con.RegisterHandler('iq', self._DiscoverInfoGetCB, 'get',
|
||||
nbxmpp.NS_DISCO_INFO)
|
||||
con.RegisterHandler('iq', self._DiscoverItemsGetCB, 'get',
|
||||
|
|
|
@ -232,61 +232,6 @@ class TimeResultReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
|||
self.time_info = t.astimezone(contact_tz()).strftime('%c')
|
||||
return True
|
||||
|
||||
class GMailQueryReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'gmail-notify'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
if not self.stanza.getTag('mailbox'):
|
||||
return
|
||||
mb = self.stanza.getTag('mailbox')
|
||||
if not mb.getAttr('url'):
|
||||
return
|
||||
self.conn.gmail_url = mb.getAttr('url')
|
||||
if mb.getNamespace() != nbxmpp.NS_GMAILNOTIFY:
|
||||
return
|
||||
self.newmsgs = mb.getAttr('total-matched')
|
||||
if not self.newmsgs:
|
||||
return
|
||||
if self.newmsgs == '0':
|
||||
return
|
||||
# there are new messages
|
||||
self.gmail_messages_list = []
|
||||
if mb.getTag('mail-thread-info'):
|
||||
gmail_messages = mb.getTags('mail-thread-info')
|
||||
for gmessage in gmail_messages:
|
||||
unread_senders = []
|
||||
for sender in gmessage.getTag('senders').getTags('sender'):
|
||||
if sender.getAttr('unread') != '1':
|
||||
continue
|
||||
if sender.getAttr('name'):
|
||||
unread_senders.append(sender.getAttr('name') + \
|
||||
'< ' + sender.getAttr('address') + '>')
|
||||
else:
|
||||
unread_senders.append(sender.getAttr('address'))
|
||||
|
||||
if not unread_senders:
|
||||
continue
|
||||
gmail_subject = gmessage.getTag('subject').getData()
|
||||
gmail_snippet = gmessage.getTag('snippet').getData()
|
||||
tid = int(gmessage.getAttr('tid'))
|
||||
if not self.conn.gmail_last_tid or \
|
||||
tid > self.conn.gmail_last_tid:
|
||||
self.conn.gmail_last_tid = tid
|
||||
self.gmail_messages_list.append({
|
||||
'From': unread_senders,
|
||||
'Subject': gmail_subject,
|
||||
'Snippet': gmail_snippet,
|
||||
'url': gmessage.getAttr('url'),
|
||||
'participation': gmessage.getAttr('participation'),
|
||||
'messages': gmessage.getAttr('messages'),
|
||||
'date': gmessage.getAttr('date')})
|
||||
self.conn.gmail_last_time = int(mb.getAttr('result-time'))
|
||||
|
||||
self.jid = app.get_jid_from_account(self.name)
|
||||
log.debug('You have %s new gmail e-mails on %s.', self.newmsgs, self.jid)
|
||||
return True
|
||||
|
||||
class RosterItemExchangeEvent(nec.NetworkIncomingEvent, HelperEvent):
|
||||
name = 'roster-item-exchange-received'
|
||||
base_network_events = []
|
||||
|
@ -671,18 +616,6 @@ class IqErrorReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
|||
self.errcode = self.stanza.getErrorCode()
|
||||
return True
|
||||
|
||||
class GmailNewMailReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'gmail-new-mail-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
if not self.stanza.getTag('new-mail'):
|
||||
return
|
||||
if self.stanza.getTag('new-mail').getNamespace() != \
|
||||
nbxmpp.NS_GMAILNOTIFY:
|
||||
return
|
||||
return True
|
||||
|
||||
class PingReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'ping-received'
|
||||
base_network_events = []
|
||||
|
|
|
@ -1,3 +1,34 @@
|
|||
from enum import IntEnum, unique
|
||||
from collections import namedtuple
|
||||
|
||||
Option = namedtuple('Option', 'kind label type value name callback data desc enabledif props')
|
||||
Option.__new__.__defaults__ = (None,) * len(Option._fields)
|
||||
|
||||
@unique
|
||||
class OptionKind(IntEnum):
|
||||
ENTRY = 0
|
||||
SWITCH = 1
|
||||
SPIN = 2
|
||||
ACTION = 3
|
||||
LOGIN = 4
|
||||
DIALOG = 5
|
||||
CALLBACK = 6
|
||||
PROXY = 7
|
||||
HOSTNAME = 8
|
||||
PRIORITY = 9
|
||||
FILECHOOSER = 10
|
||||
CHANGEPASSWORD = 11
|
||||
GPG = 12
|
||||
|
||||
@unique
|
||||
class OptionType(IntEnum):
|
||||
ACCOUNT_CONFIG = 0
|
||||
CONFIG = 1
|
||||
BOOL = 2
|
||||
ACTION = 3
|
||||
DIALOG = 4
|
||||
|
||||
|
||||
THANKS = u"""\
|
||||
Alexander Futász
|
||||
Alexander V. Butenko
|
||||
|
|
|
@ -76,54 +76,8 @@ def base28(n):
|
|||
else:
|
||||
return base28_chr[n]
|
||||
|
||||
def add_entropy_sources_OpenSSL():
|
||||
# Other possibly variable data. This are very low quality sources of
|
||||
# entropy, but some of them are installation dependent and can be hard
|
||||
# to guess for the attacker.
|
||||
# Data available on all platforms Unix, Windows
|
||||
sources = [sys.argv, sys.builtin_module_names,
|
||||
sys.copyright, sys.getfilesystemencoding(), sys.hexversion,
|
||||
sys.modules, sys.path, sys.version, sys.api_version,
|
||||
os.environ, os.getcwd(), os.getpid()]
|
||||
|
||||
for s in sources:
|
||||
OpenSSL.rand.add(str(s).encode('utf-8'), 1)
|
||||
|
||||
# On Windows add the current contents of the screen to the PRNG state.
|
||||
# if os.name == 'nt':
|
||||
# OpenSSL.rand.screen()
|
||||
# The /proc filesystem on POSIX systems contains many random variables:
|
||||
# memory statistics, interrupt counts, network packet counts
|
||||
if os.name == 'posix':
|
||||
dirs = ['/proc', '/proc/net', '/proc/self']
|
||||
for d in dirs:
|
||||
if os.access(d, os.R_OK):
|
||||
for filename in os.listdir(d):
|
||||
OpenSSL.rand.add(filename.encode('utf-8'), 0)
|
||||
try:
|
||||
with open(d + os.sep + filename, "r") as fp:
|
||||
# Limit the ammount of read bytes, in case a memory
|
||||
# file was opened
|
||||
OpenSSL.rand.add(str(fp.read(5000)).encode('utf-8'),
|
||||
1)
|
||||
except:
|
||||
# Ignore all read and access errors
|
||||
pass
|
||||
|
||||
PYOPENSSL_PRNG_PRESENT = False
|
||||
try:
|
||||
import OpenSSL.rand
|
||||
PYOPENSSL_PRNG_PRESENT = True
|
||||
except ImportError:
|
||||
# PyOpenSSL PRNG not available
|
||||
pass
|
||||
|
||||
def random_bytes(bytes_):
|
||||
if PYOPENSSL_PRNG_PRESENT:
|
||||
OpenSSL.rand.add(os.urandom(bytes_), bytes_)
|
||||
return OpenSSL.rand.bytes(bytes_)
|
||||
else:
|
||||
return os.urandom(bytes_)
|
||||
return os.urandom(bytes_)
|
||||
|
||||
def generate_nonce():
|
||||
return random_bytes(8)
|
||||
|
|
|
@ -121,8 +121,10 @@ class JingleRTPContent(JingleContent):
|
|||
InformationEvent(
|
||||
None, conn=self.session.connection, level='error',
|
||||
pri_txt=_('%s configuration error') % text.capitalize(),
|
||||
sec_txt=_('Couldn’t setup %s. Check your configuration.\n\n'
|
||||
'Pipeline was:\n%s\n\nError was:\n%s') % (text, pipeline, str(e))))
|
||||
sec_txt=_('Couldn’t setup %(text)s. Check your '
|
||||
'configuration.\n\nPipeline was:\n%(pipeline)s\n\n'
|
||||
'Error was:\n%(error)s') % {'text': text,
|
||||
'pipeline': pipeline, 'error': str(e)}))
|
||||
raise JingleContentSetupException
|
||||
|
||||
def add_remote_candidates(self, candidates):
|
||||
|
@ -228,9 +230,9 @@ class JingleRTPContent(JingleContent):
|
|||
InformationEvent(
|
||||
None, conn=self.session.connection, level='error',
|
||||
pri_txt=_('GStreamer error'),
|
||||
sec_txt=_('Error: %s\nDebug: %s' %
|
||||
(message.get_structure().get_value('gerror'),
|
||||
message.get_structure().get_value('debug')))))
|
||||
sec_txt=_('Error: %(error)s\nDebug: %(debug)s' % {
|
||||
'error': message.get_structure().get_value('gerror'),
|
||||
'debug': message.get_structure().get_value('debug')})))
|
||||
|
||||
sink_pad = self.p2psession.get_property('sink-pad')
|
||||
|
||||
|
|
|
@ -27,6 +27,9 @@
|
|||
import os
|
||||
import logging
|
||||
import gi
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from gajim.common import app
|
||||
|
||||
__all__ = ['get_password', 'save_password']
|
||||
|
@ -78,10 +81,15 @@ class LibSecretPasswordStorage(PasswordStorage):
|
|||
def save_password(self, account_name, password, update=True):
|
||||
server = app.config.get_per('accounts', account_name, 'hostname')
|
||||
user = app.config.get_per('accounts', account_name, 'name')
|
||||
display_name = _('XMPP account %s@%s') % (user, server)
|
||||
display_name = _('XMPP account %s') % user + '@' + server
|
||||
attributes = {'user': user, 'server': server, 'protocol': 'xmpp'}
|
||||
return self.Secret.password_store_sync(self.GAJIM_SCHEMA, attributes,
|
||||
self.Secret.COLLECTION_DEFAULT, display_name, password or '', None)
|
||||
try:
|
||||
return self.Secret.password_store_sync(
|
||||
self.GAJIM_SCHEMA, attributes, self.Secret.COLLECTION_DEFAULT,
|
||||
display_name, password or '', None)
|
||||
except GLib.Error as error:
|
||||
log.error(error)
|
||||
return False
|
||||
|
||||
|
||||
class SecretWindowsPasswordStorage(PasswordStorage):
|
||||
|
|
|
@ -23,7 +23,7 @@ import functools
|
|||
log = logging.getLogger('gajim.c.resolver')
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.path.append('..')
|
||||
sys.path.append('../..')
|
||||
from gajim.common import i18n
|
||||
from gajim.common import configpaths
|
||||
configpaths.gajimpaths.init(None)
|
||||
|
|
1268
gajim/config.py
|
@ -918,13 +918,12 @@ class ConversationTextview(GObject.GObject):
|
|||
if special_text.startswith(scheme):
|
||||
text_is_valid_uri = True
|
||||
|
||||
possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS
|
||||
if iter_:
|
||||
end_iter = iter_
|
||||
else:
|
||||
end_iter = buffer_.get_end_iter()
|
||||
|
||||
pixbuf = emoticons.get_pixbuf(possible_emot_ascii_caps)
|
||||
pixbuf = emoticons.get_pixbuf(special_text)
|
||||
if app.config.get('emoticons_theme') and pixbuf and graphics:
|
||||
# it's an emoticon
|
||||
anchor = buffer_.create_child_anchor(end_iter)
|
||||
|
|
19
gajim/data/Makefile.am
Normal file
|
@ -0,0 +1,19 @@
|
|||
SUBDIRS = gui emoticons iconsets moods activities
|
||||
@INTLTOOL_DESKTOP_RULE@
|
||||
|
||||
soundsdir = $(gajim_srcdir)/data/sounds
|
||||
sounds_DATA = $(srcdir)/sounds/*.wav
|
||||
|
||||
styledir = $(gajim_srcdir)/data/style
|
||||
style_DATA = $(srcdir)/style/*.css
|
||||
|
||||
otherdir = $(gajim_srcdir)/data/other
|
||||
other_DATA = other/servers.xml other/dh4096.pem
|
||||
# other/cacert.pem is used only on Windows. On Unix platforms
|
||||
# use CA certificates installed in /etc/ssl/certs
|
||||
|
||||
EXTRA_DIST = $(sounds_DATA) \
|
||||
$(style_DATA) \
|
||||
$(other_DATA)
|
||||
|
||||
MAINTAINERCLEANFILES = Makefile.in
|
|
@ -1,4 +1,4 @@
|
|||
activitiesdir = $(pkgdatadir)/data/activities
|
||||
activitiesdir = $(gajim_srcdir)/data/activities
|
||||
nobase_dist_activities_DATA = $(srcdir)/*/*/*
|
||||
|
||||
MAINTAINERCLEANFILES = Makefile.in
|
Before Width: | Height: | Size: 909 B After Width: | Height: | Size: 909 B |
Before Width: | Height: | Size: 854 B After Width: | Height: | Size: 854 B |
Before Width: | Height: | Size: 842 B After Width: | Height: | Size: 842 B |
Before Width: | Height: | Size: 1,005 B After Width: | Height: | Size: 1,005 B |
Before Width: | Height: | Size: 957 B After Width: | Height: | Size: 957 B |
Before Width: | Height: | Size: 916 B After Width: | Height: | Size: 916 B |
Before Width: | Height: | Size: 808 B After Width: | Height: | Size: 808 B |
Before Width: | Height: | Size: 749 B After Width: | Height: | Size: 749 B |
Before Width: | Height: | Size: 980 B After Width: | Height: | Size: 980 B |
Before Width: | Height: | Size: 891 B After Width: | Height: | Size: 891 B |
Before Width: | Height: | Size: 706 B After Width: | Height: | Size: 706 B |
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
Before Width: | Height: | Size: 918 B After Width: | Height: | Size: 918 B |
Before Width: | Height: | Size: 712 B After Width: | Height: | Size: 712 B |
Before Width: | Height: | Size: 868 B After Width: | Height: | Size: 868 B |
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
Before Width: | Height: | Size: 844 B After Width: | Height: | Size: 844 B |
Before Width: | Height: | Size: 969 B After Width: | Height: | Size: 969 B |
Before Width: | Height: | Size: 1,016 B After Width: | Height: | Size: 1,016 B |
Before Width: | Height: | Size: 912 B After Width: | Height: | Size: 912 B |
Before Width: | Height: | Size: 901 B After Width: | Height: | Size: 901 B |
Before Width: | Height: | Size: 957 B After Width: | Height: | Size: 957 B |
Before Width: | Height: | Size: 723 B After Width: | Height: | Size: 723 B |
Before Width: | Height: | Size: 625 B After Width: | Height: | Size: 625 B |
Before Width: | Height: | Size: 894 B After Width: | Height: | Size: 894 B |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 854 B After Width: | Height: | Size: 854 B |
Before Width: | Height: | Size: 900 B After Width: | Height: | Size: 900 B |
Before Width: | Height: | Size: 846 B After Width: | Height: | Size: 846 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 665 B After Width: | Height: | Size: 665 B |
Before Width: | Height: | Size: 1,013 B After Width: | Height: | Size: 1,013 B |
Before Width: | Height: | Size: 759 B After Width: | Height: | Size: 759 B |
Before Width: | Height: | Size: 689 B After Width: | Height: | Size: 689 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 793 B After Width: | Height: | Size: 793 B |
Before Width: | Height: | Size: 1,012 B After Width: | Height: | Size: 1,012 B |
Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 981 B |
Before Width: | Height: | Size: 589 B After Width: | Height: | Size: 589 B |
Before Width: | Height: | Size: 907 B After Width: | Height: | Size: 907 B |
Before Width: | Height: | Size: 752 B After Width: | Height: | Size: 752 B |
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 917 B |
Before Width: | Height: | Size: 882 B After Width: | Height: | Size: 882 B |
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 703 B |
Before Width: | Height: | Size: 997 B After Width: | Height: | Size: 997 B |
Before Width: | Height: | Size: 967 B After Width: | Height: | Size: 967 B |
Before Width: | Height: | Size: 839 B After Width: | Height: | Size: 839 B |
Before Width: | Height: | Size: 719 B After Width: | Height: | Size: 719 B |
Before Width: | Height: | Size: 866 B After Width: | Height: | Size: 866 B |
Before Width: | Height: | Size: 793 B After Width: | Height: | Size: 793 B |
Before Width: | Height: | Size: 990 B After Width: | Height: | Size: 990 B |
Before Width: | Height: | Size: 913 B After Width: | Height: | Size: 913 B |
Before Width: | Height: | Size: 921 B After Width: | Height: | Size: 921 B |
Before Width: | Height: | Size: 964 B After Width: | Height: | Size: 964 B |
Before Width: | Height: | Size: 621 B After Width: | Height: | Size: 621 B |
Before Width: | Height: | Size: 956 B After Width: | Height: | Size: 956 B |
Before Width: | Height: | Size: 958 B After Width: | Height: | Size: 958 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 955 B After Width: | Height: | Size: 955 B |
Before Width: | Height: | Size: 836 B After Width: | Height: | Size: 836 B |
Before Width: | Height: | Size: 792 B After Width: | Height: | Size: 792 B |
Before Width: | Height: | Size: 651 B After Width: | Height: | Size: 651 B |
Before Width: | Height: | Size: 865 B After Width: | Height: | Size: 865 B |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 868 B After Width: | Height: | Size: 868 B |
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 982 B |
Before Width: | Height: | Size: 960 B After Width: | Height: | Size: 960 B |
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 696 B |
Before Width: | Height: | Size: 897 B After Width: | Height: | Size: 897 B |
Before Width: | Height: | Size: 773 B After Width: | Height: | Size: 773 B |
Before Width: | Height: | Size: 825 B After Width: | Height: | Size: 825 B |
Before Width: | Height: | Size: 962 B After Width: | Height: | Size: 962 B |
Before Width: | Height: | Size: 797 B After Width: | Height: | Size: 797 B |
Before Width: | Height: | Size: 345 B After Width: | Height: | Size: 345 B |