gajim-plural/gajim/application.py

501 lines
18 KiB
Python
Raw Normal View History

2018-09-05 00:14:27 +02:00
# Copyright (C) 2003-2017 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org>
# Copyright (C) 2005 Alex Podaras <bigpod AT gmail.com>
# Norman Rasmussen <norman AT rasmussen.co.za>
# Stéphan Kochen <stephan AT kochen.nl>
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
# Alex Mauer <hawke AT hawkesnest.net>
# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
# Stefan Bethge <stefan AT lanpartei.de>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
# James Newton <redshodan AT gmail.com>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
# Julien Pivotto <roidelapluie AT gmail.com>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
# Copyright (C) 2016-2017 Emmanuel Gil Peyrot <linkmauve AT linkmauve.fr>
# Philipp Hörist <philipp AT hoerist.com>
#
# 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 sys
from datetime import datetime
from pathlib import Path
from urllib.parse import unquote
from gi.repository import GLib, Gio, Gtk
2018-10-07 02:05:07 +02:00
import gajim
2018-04-25 20:59:23 +02:00
from gajim.common import app
from gajim.common import configpaths
from gajim.common import logging_helpers
2018-04-25 21:15:48 +02:00
from gajim.common import exceptions
from gajim.common import caps_cache
from gajim.common import logger
from gajim.common.i18n import _
class GajimApplication(Gtk.Application):
'''Main class handling activation and command line.'''
def __init__(self):
2018-09-05 00:14:27 +02:00
flags = (Gio.ApplicationFlags.HANDLES_COMMAND_LINE |
Gio.ApplicationFlags.HANDLES_OPEN)
Gtk.Application.__init__(self,
application_id='org.gajim.Gajim',
flags=flags)
self.add_main_option(
'version',
ord('V'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Show the application\'s version'))
self.add_main_option(
'quiet',
ord('q'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Show only critical errors'))
self.add_main_option(
'separate',
ord('s'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Separate profile files completely '
'(even history database and plugins)'))
self.add_main_option(
'verbose',
ord('v'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Print XML stanzas and other debug information'))
self.add_main_option(
'profile',
ord('p'),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Use defined profile in configuration directory'),
'NAME')
self.add_main_option(
'config-path',
ord('c'),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Set configuration directory'),
'PATH')
self.add_main_option(
'loglevel',
ord('l'),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Configure logging system'),
'LEVEL')
self.add_main_option(
'warnings',
ord('w'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Show all warnings'))
self.add_main_option(
'ipython',
ord('i'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Open IPython shell'))
self.add_main_option(
'show-next-pending-event',
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Pops up a window with the next pending event'))
self.add_main_option(
'start-chat', 0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Start a new chat'))
self.add_main_option(
'simulate-network-lost',
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Simulate loss of connectivity'))
self.add_main_option(
'simulate-network-connected',
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Simulate regaining connectivity'))
self.add_main_option_entries(self._get_remaining_entry())
self.connect('handle-local-options', self._handle_local_options)
self.connect('command-line', self._handle_remote_options)
self.connect('startup', self._startup)
self.connect('activate', self._activate)
self.interface = None
GLib.set_prgname('gajim')
if GLib.get_application_name() != 'Gajim':
GLib.set_application_name('Gajim')
@staticmethod
def _get_remaining_entry():
option = GLib.OptionEntry()
option.arg = GLib.OptionArg.STRING_ARRAY
option.arg_data = None
option.arg_description = ('[URI …]')
option.flags = GLib.OptionFlags.NONE
option.long_name = GLib.OPTION_REMAINING
option.short_name = 0
return [option]
def _startup(self, application):
# Create and initialize Application Paths & Databases
app.detect_dependencies()
configpaths.create_paths()
try:
app.logger = logger.Logger()
caps_cache.initialize(app.logger)
except exceptions.DatabaseMalformed as error:
dlg = Gtk.MessageDialog(
transient_for=None,
destroy_with_parent=True,
modal=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text=_('Database Error'))
dlg.format_secondary_text(str(error))
dlg.run()
dlg.destroy()
sys.exit()
from gajim.gtk.util import load_user_iconsets
load_user_iconsets()
# Set Application Menu
app.app = self
2018-09-08 08:40:42 +02:00
from gajim.gtk.util import get_builder
builder = get_builder('application_menu.ui')
menubar = builder.get_object("menubar")
appmenu = builder.get_object("appmenu")
if app.prefers_app_menu():
self.set_app_menu(appmenu)
else:
# Add it to the menubar instead
menubar.prepend_submenu('Gajim', appmenu)
self.set_menubar(menubar)
def _activate(self, application):
if self.interface is not None:
self.interface.roster.window.present()
return
from gajim.gui_interface import Interface
self.interface = Interface()
self.interface.run(self)
self.add_actions()
from gajim import gui_menu_builder
gui_menu_builder.build_accounts_menu()
def _open_uris(self, uris):
for uri in uris:
app.log('uri_handler').info('open %s', uri)
if not uri.startswith('xmpp:'):
continue
# remove xmpp:
uri = uri[5:]
try:
jid, cmd = uri.split('?')
except ValueError:
# No query argument
jid, cmd = uri, 'message'
if cmd == 'join':
self.interface.join_gc_minimal(None, jid)
elif cmd == 'roster':
self.activate_action('add-contact', GLib.Variant('s', jid))
elif cmd.startswith('message'):
attributes = cmd.split(';')
message = None
for key in attributes:
2018-09-05 00:14:27 +02:00
if not key.startswith('body'):
continue
try:
message = unquote(key.split('=')[1])
except Exception:
app.log('uri_handler').error('Invalid URI: %s', cmd)
accounts = list(app.connections.keys())
if not accounts:
continue
if len(accounts) == 1:
app.interface.new_chat_from_jid(accounts[0], jid, message)
else:
self.activate_action('start-chat')
start_chat_window = app.interface.instances['start_chat']
start_chat_window.search_entry.set_text(jid)
def do_shutdown(self, *args):
Gtk.Application.do_shutdown(self)
# Shutdown GUI and save config
if hasattr(self.interface, 'roster') and self.interface.roster:
self.interface.roster.prepare_quit()
# Commit any outstanding SQL transactions
app.logger.commit()
def _handle_remote_options(self, application, command_line):
# Parse all options that should be executed on a remote instance
options = command_line.get_options_dict()
remote_commands = [
'ipython',
'show-next-pending-event',
'start-chat',
]
remaining = options.lookup_value(GLib.OPTION_REMAINING,
GLib.VariantType.new('as'))
for cmd in remote_commands:
if options.contains(cmd):
self.activate_action(cmd)
return 0
if options.contains('simulate-network-lost'):
app.interface.network_status_changed(None, False)
return 0
if options.contains('simulate-network-connected'):
app.interface.network_status_changed(None, True)
return 0
if remaining is not None:
self._open_uris(remaining.unpack())
return 0
self.activate()
return 0
def _handle_local_options(self,
application: Gtk.Application,
options: GLib.VariantDict) -> int:
# Parse all options that have to be executed before ::startup
2018-10-07 02:05:07 +02:00
if options.contains('version'):
print(gajim.__version__)
return 0
if options.contains('profile'):
# Incorporate profile name into application id
# to have a single app instance for each profile.
profile = options.lookup_value('profile').get_string()
app_id = '%s.%s' % (self.get_application_id(), profile)
self.set_application_id(app_id)
configpaths.set_profile(profile)
if options.contains('separate'):
configpaths.set_separation(True)
if options.contains('config-path'):
path = options.lookup_value('config-path').get_string()
configpaths.set_config_root(path)
configpaths.init()
if app.get_win_debug_mode():
# Redirect has to happen before logging init
self._redirect_output()
logging_helpers.init()
logging_helpers.set_verbose()
else:
logging_helpers.init()
if options.contains('quiet'):
logging_helpers.set_quiet()
if options.contains('verbose'):
logging_helpers.set_verbose()
if options.contains('loglevel'):
loglevel = options.lookup_value('loglevel').get_string()
logging_helpers.set_loglevels(loglevel)
if options.contains('warnings'):
self.show_warnings()
return -1
def show_warnings(self):
import traceback
import warnings
def warn_with_traceback(message, category, filename, lineno,
file=None, line=None):
traceback.print_stack(file=sys.stderr)
sys.stderr.write(warnings.formatwarning(message, category,
filename, lineno, line))
warnings.showwarning = warn_with_traceback
warnings.filterwarnings(action="always")
@staticmethod
def _redirect_output():
debug_folder = Path(configpaths.get('DEBUG'))
date = datetime.today().strftime('%Y-%m-%d')
filename = '%s-debug.log' % date
fd = open(debug_folder / filename, 'a')
sys.stderr = sys.stdout = fd
def add_actions(self):
''' Build Application Actions '''
2018-05-21 21:18:38 +02:00
from gajim import app_actions
# General Stateful Actions
act = Gio.SimpleAction.new_stateful(
'merge', None,
GLib.Variant.new_boolean(app.config.get('mergeaccounts')))
2018-05-21 21:18:38 +02:00
act.connect('change-state', app_actions.on_merge_accounts)
self.add_action(act)
act = Gio.SimpleAction.new_stateful(
'agent', None,
GLib.Variant.new_boolean(app.config.get('use_gpg_agent')))
self.add_action(act)
# General Actions
2018-05-21 21:18:38 +02:00
general_actions = [
('quit', app_actions.on_quit),
('accounts', app_actions.on_accounts),
('add-account', app_actions.on_add_account),
('join-groupchat', app_actions.on_join_gc),
2018-05-21 21:18:38 +02:00
('manage-proxies', app_actions.on_manage_proxies),
('start-chat', app_actions.on_new_chat),
('bookmarks', app_actions.on_manage_bookmarks),
('history-manager', app_actions.on_history_manager),
('preferences', app_actions.on_preferences),
('plugins', app_actions.on_plugins),
('file-transfer', app_actions.on_file_transfers),
('history', app_actions.on_history),
('shortcuts', app_actions.on_keyboard_shortcuts),
('features', app_actions.on_features),
('content', app_actions.on_contents),
('about', app_actions.on_about),
('faq', app_actions.on_faq),
('ipython', app_actions.toggle_ipython),
('show-next-pending-event', app_actions.show_next_pending_event),
]
act = Gio.SimpleAction.new('add-contact', GLib.VariantType.new('s'))
2018-05-21 21:18:38 +02:00
act.connect("activate", app_actions.on_add_contact_jid)
self.add_action(act)
act = Gio.SimpleAction.new('copy-link', GLib.VariantType.new('s'))
act.connect("activate", app_actions.copy_link)
self.add_action(act)
act = Gio.SimpleAction.new('open-link', GLib.VariantType.new('as'))
act.connect("activate", app_actions.open_link)
self.add_action(act)
act = Gio.SimpleAction.new('browse-history',
GLib.VariantType.new('a{sv}'))
act.connect("activate", app_actions.on_browse_history)
self.add_action(act)
2018-05-21 21:18:38 +02:00
for action in general_actions:
action_name, func = action
act = Gio.SimpleAction.new(action_name, None)
act.connect("activate", func)
self.add_action(act)
accounts_list = sorted(app.config.get_per('accounts'))
2018-05-21 18:57:18 +02:00
if not accounts_list:
return
if len(accounts_list) > 1:
for acc in accounts_list:
self.add_account_actions(acc)
else:
self.add_account_actions(accounts_list[0])
2018-05-21 21:18:38 +02:00
def _get_account_actions(self, account):
2018-09-05 00:14:27 +02:00
from gajim import app_actions as a
2018-05-21 21:18:38 +02:00
2018-05-20 20:15:08 +02:00
if account == 'Local':
2018-05-21 21:18:38 +02:00
return [
2018-09-05 00:14:27 +02:00
('-xml-console', a.on_xml_console, 'always', 's')
2018-05-21 21:18:38 +02:00
]
return [
2018-09-05 00:14:27 +02:00
('-start-single-chat', a.on_single_message, 'online', 's'),
('-start-chat', a.start_chat, 'online', 'as'),
('-join-groupchat', a.on_join_gc, 'online', 'as'),
('-add-contact', a.on_add_contact, 'online', 'as'),
2018-09-05 00:14:27 +02:00
('-services', a.on_service_disco, 'online', 's'),
('-profile', a.on_profile, 'feature', 's'),
('-xml-console', a.on_xml_console, 'always', 's'),
('-server-info', a.on_server_info, 'online', 's'),
('-archive', a.on_mam_preferences, 'feature', 's'),
('-sync-history', a.on_history_sync, 'online', 's'),
('-privacylists', a.on_privacy_lists, 'feature', 's'),
('-send-server-message', a.on_send_server_message, 'online', 's'),
('-set-motd', a.on_set_motd, 'online', 's'),
('-update-motd', a.on_update_motd, 'online', 's'),
('-delete-motd', a.on_delete_motd, 'online', 's'),
('-activate-bookmark', a.on_activate_bookmark, 'online', 'a{sv}'),
('-open-event', a.on_open_event, 'always', 'a{sv}'),
('-import-contacts', a.on_import_contacts, 'online', 's'),
2018-05-21 21:18:38 +02:00
]
def add_account_actions(self, account):
for action in self._get_account_actions(account):
action_name, func, state, type_ = action
action_name = account + action_name
if self.lookup_action(action_name):
# We already added this action
continue
act = Gio.SimpleAction.new(
action_name, GLib.VariantType.new(type_))
act.connect("activate", func)
if state != 'always':
act.set_enabled(False)
self.add_action(act)
def remove_account_actions(self, account):
2018-05-21 21:18:38 +02:00
for action in self._get_account_actions(account):
action_name = account + action[0]
self.remove_action(action_name)
def set_account_actions_state(self, account, new_state=False):
2018-05-21 21:18:38 +02:00
for action in self._get_account_actions(account):
action_name, _, state, _ = action
if not new_state and state in ('online', 'feature'):
# We go offline
self.lookup_action(account + action_name).set_enabled(False)
elif new_state and state == 'online':
# We go online
self.lookup_action(account + action_name).set_enabled(True)