gajim3/gajim/gui_interface.py

2105 lines
83 KiB
Python

# Copyright (C) 2003-2014 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>
#
# 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 os
import sys
import time
import json
import logging
from functools import partial
from threading import Thread
from datetime import datetime
from importlib.util import find_spec
from packaging.version import Version as V
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Soup
from nbxmpp import idlequeue
from nbxmpp import Hashes2
from gajim.common import app
from gajim.common import events
from gajim.common.dbus import location
from gajim.common.dbus import logind
from gajim.common.dbus import music_track
from gajim import gui_menu_builder
from gajim.dialog_messages import get_dialog
from gajim.chat_control_base import ChatControlBase
from gajim.chat_control import ChatControl
from gajim.groupchat_control import GroupchatControl
from gajim.privatechat_control import PrivateChatControl
from gajim.message_window import MessageWindowMgr
from gajim.session import ChatControlSession
from gajim.common import idle
from gajim.common.zeroconf import connection_zeroconf
from gajim.common import proxy65_manager
from gajim.common import socks5
from gajim.common import helpers
from gajim.common import passwords
from gajim.common.helpers import ask_for_status_message
from gajim.common.helpers import get_group_chat_nick
from gajim.common.structs import MUCData
from gajim.common.structs import OutgoingMessage
from gajim.common.nec import NetworkEvent
from gajim.common.i18n import _
from gajim.common.client import Client
from gajim.common.const import Display
from gajim.common.const import JingleState
from gajim.common.file_props import FilesProp
from gajim.common.connection_handlers_events import InformationEvent
from gajim import roster_window
from gajim.common import ged
from gajim.common.exceptions import FileError
from gajim.gui.avatar import AvatarStorage
from gajim.gui.notification import Notification
from gajim.gui.dialogs import DialogButton
from gajim.gui.dialogs import ErrorDialog
from gajim.gui.dialogs import WarningDialog
from gajim.gui.dialogs import InformationDialog
from gajim.gui.dialogs import ConfirmationDialog
from gajim.gui.dialogs import ConfirmationCheckDialog
from gajim.gui.dialogs import InputDialog
from gajim.gui.dialogs import PassphraseDialog
from gajim.gui.filechoosers import FileChooserDialog
from gajim.gui.filetransfer import FileTransfersWindow
from gajim.gui.filetransfer_progress import FileTransferProgress
from gajim.gui.roster_item_exchange import RosterItemExchangeWindow
from gajim.gui.util import get_show_in_roster
from gajim.gui.util import get_show_in_systray
from gajim.gui.util import open_window
from gajim.gui.util import get_app_window
from gajim.gui.util import get_app_windows
from gajim.gui.util import get_color_for_account
from gajim.gui.const import ControlType
log = logging.getLogger('gajim.interface')
class Interface:
################################################################################
### Methods handling events from connection
################################################################################
def handle_event_db_error(self, unused, error):
#('DB_ERROR', account, error)
if self.db_error_dialog:
return
self.db_error_dialog = ErrorDialog(_('Database Error'), error)
def destroyed(win):
self.db_error_dialog = None
self.db_error_dialog.connect('destroy', destroyed)
@staticmethod
def handle_event_information(obj):
if not obj.popup:
return
if obj.dialog_name is not None:
get_dialog(obj.dialog_name, *obj.args, **obj.kwargs)
return
if obj.level == 'error':
cls = ErrorDialog
elif obj.level == 'warn':
cls = WarningDialog
elif obj.level == 'info':
cls = InformationDialog
else:
return
cls(obj.pri_txt, GLib.markup_escape_text(obj.sec_txt))
@staticmethod
def raise_dialog(name, *args, **kwargs):
get_dialog(name, *args, **kwargs)
@staticmethod
def handle_event_http_auth(obj):
# ('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg))
def _response(account, answer):
obj.conn.get_module('HTTPAuth').build_http_auth_answer(
obj.stanza, answer)
account = obj.conn.name
message = _('HTTP (%(method)s) Authorization '
'for %(url)s (ID: %(id)s)') % {
'method': obj.method,
'url': obj.url,
'id': obj.iq_id}
sec_msg = _('Do you accept this request?')
if app.get_number_of_connected_accounts() > 1:
sec_msg = _('Do you accept this request (account: %s)?') % account
if obj.msg:
sec_msg = obj.msg + '\n' + sec_msg
message = message + '\n' + sec_msg
ConfirmationDialog(
_('Authorization Request'),
_('HTTP Authorization Request'),
message,
[DialogButton.make('Cancel',
text=_('_No'),
callback=_response,
args=[obj, 'no']),
DialogButton.make('Accept',
callback=_response,
args=[obj, 'yes'])]).show()
def handle_event_iq_error(self, event):
ctrl = self.msg_win_mgr.get_control(event.properties.jid.bare,
event.account)
if ctrl and ctrl.is_groupchat:
ctrl.add_info_message('Error: %s' % event.properties.error)
@staticmethod
def handle_event_connection_lost(obj):
# ('CONNECTION_LOST', account, [title, text])
account = obj.conn.name
app.notification.popup(
_('Connection Failed'), account, account,
'connection-lost', 'gajim-connection_lost', obj.title, obj.msg)
@staticmethod
def unblock_signed_in_notifications(account):
app.block_signed_in_notifications[account] = False
def handle_event_status(self, event):
if event.show in ('offline', 'error'):
# TODO: Close all account windows
pass
if event.show == 'offline':
app.block_signed_in_notifications[event.account] = True
else:
# 30 seconds after we change our status to sth else than offline
# we stop blocking notifications of any kind
# this prevents from getting the roster items as 'just signed in'
# contacts. 30 seconds should be enough time
GLib.timeout_add_seconds(30,
self.unblock_signed_in_notifications,
event.account)
def handle_event_presence(self, obj):
# 'NOTIFY' (account, (jid, status, status message, resource,
# priority, timestamp))
#
# Contact changed show
account = obj.conn.name
jid = obj.jid
if app.jid_is_transport(jid):
# It must be an agent
# transport just signed in/out, don't show
# popup notifications for 30s
account_jid = account + '/' + jid
app.block_signed_in_notifications[account_jid] = True
GLib.timeout_add_seconds(30, self.unblock_signed_in_notifications,
account_jid)
ctrl = self.msg_win_mgr.get_control(jid, account)
if ctrl and ctrl.session and len(obj.contact_list) > 1:
ctrl.remove_session(ctrl.session)
@staticmethod
def handle_event_read_state_sync(event):
if event.type.is_groupchat:
control = app.get_groupchat_control(
event.account, event.jid.bare)
if control is None:
log.warning('Groupchat control not found')
return
jid = event.jid.bare
types = ['printed_gc_msg', 'printed_marked_gc_msg']
else:
types = ['chat', 'pm', 'printed_chat', 'printed_pm']
jid = event.jid
control = app.interface.msg_win_mgr.get_control(jid, event.account)
# Compare with control.last_msg_id.
events_ = app.events.get_events(event.account, jid, types)
if not events_:
log.warning('No Events')
return
if event.type.is_groupchat:
id_ = events_[-1].stanza_id or events_[-1].message_id
else:
id_ = events_[-1].message_id
if id_ != event.marker_id:
return
if not app.events.remove_events(event.account, jid, types=types):
# There were events to remove
if control is not None:
control.redraw_after_event_removed(event.jid)
@staticmethod
def handle_event_msgsent(obj):
if not obj.play_sound:
return
enabled = app.settings.get_soundevent_settings('message_sent')['enabled']
if enabled:
if isinstance(obj.jid, list) and len(obj.jid) > 1:
return
helpers.play_sound('message_sent')
@staticmethod
def handle_event_msgnotsent(obj):
#('MSGNOTSENT', account, (jid, ierror_msg, msg, time, session))
msg = _('error while sending %(message)s ( %(error)s )') % {
'message': obj.message, 'error': obj.error}
if not obj.session:
# No session. This can happen when sending a message from
# gajim-remote
log.warning(msg)
return
obj.session.roster_message(obj.jid, msg, obj.time_, obj.conn.name,
msg_type='error')
def handle_event_subscribe_presence(self, obj):
#('SUBSCRIBE', account, (jid, text, user_nick)) user_nick is JEP-0172
account = obj.conn.name
if helpers.allow_popup_window(account) or not self.systray_enabled:
open_window('SubscriptionRequest',
account=account,
jid=obj.jid,
text=obj.status,
user_nick=obj.user_nick)
return
event = events.SubscriptionRequestEvent(obj.status, obj.user_nick)
self.add_event(account, obj.jid, event)
if helpers.allow_showing_notification(account):
event_type = _('Subscription request')
app.notification.popup(
event_type, obj.jid, account, 'subscription_request',
'gajim-subscription_request', event_type, obj.jid)
def handle_event_subscribed_presence(self, event):
bare_jid = event.jid.bare
resource = event.jid.resource
if bare_jid in app.contacts.get_jid_list(event.account):
contact = app.contacts.get_first_contact_from_jid(event.account,
bare_jid)
contact.resource = resource
self.roster.remove_contact_from_groups(contact.jid,
event.account,
[_('Not in contact list'),
_('Observers')],
update=False)
else:
name = event.jid.localpart
name = name.split('%', 1)[0]
contact = app.contacts.create_contact(jid=bare_jid,
account=event.account,
name=name,
groups=[],
show='online',
status='online',
ask='to',
resource=resource)
app.contacts.add_contact(event.account, contact)
self.roster.add_contact(bare_jid, event.account)
app.notification.popup(
None,
bare_jid,
event.account,
title=_('Authorization accepted'),
text=_('The contact "%(jid)s" has authorized you'
' to see their status.') % {'jid': event.jid})
def show_unsubscribed_dialog(self, account, contact):
def _remove():
self.roster.on_req_usub(None, [(contact, account)])
name = contact.get_shown_name()
jid = contact.jid
ConfirmationDialog(
_('Subscription Removed'),
_('%(name)s (%(jid)s) has removed subscription from you') % {
'name': name, 'jid': jid},
_('You will always see this contact as offline.\n'
'Do you want to remove them from your contact list?'),
[DialogButton.make('Cancel',
text=_('_No')),
DialogButton.make('Remove',
callback=_remove)]).show()
# FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does
# not show deny
def handle_event_unsubscribed_presence(self, obj):
#('UNSUBSCRIBED', account, jid)
account = obj.conn.name
contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
if not contact:
return
if helpers.allow_popup_window(account) or not self.systray_enabled:
self.show_unsubscribed_dialog(account, contact)
return
event = events.UnsubscribedEvent(contact)
self.add_event(account, obj.jid, event)
if helpers.allow_showing_notification(account):
event_type = _('Unsubscribed')
app.notification.popup(
event_type, obj.jid, account,
'unsubscribed', 'gajim-unsubscribed',
event_type, obj.jid)
def handle_event_gc_decline(self, event):
gc_control = self.msg_win_mgr.get_gc_control(str(event.muc),
event.account)
if gc_control:
if event.reason:
gc_control.add_info_message(
_('%(jid)s declined the invitation: %(reason)s') % {
'jid': event.from_, 'reason': event.reason})
else:
gc_control.add_info_message(
_('%(jid)s declined the invitation') % {
'jid': event.from_})
def handle_event_gc_invitation(self, event):
event = events.GcInvitationtEvent(event)
if (helpers.allow_popup_window(event.account) or
not self.systray_enabled):
open_window('GroupChatInvitation',
account=event.account,
event=event)
return
self.add_event(event.account, str(event.from_), event)
if helpers.allow_showing_notification(event.account):
contact_name = event.get_inviter_name()
event_type = _('Group Chat Invitation')
text = _('%(contact)s invited you to %(chat)s') % {
'contact': contact_name, 'chat': event.info.muc_name}
app.notification.popup(event_type,
str(event.from_),
event.account,
'gc-invitation',
'gajim-gc_invitation',
event_type,
text,
room_jid=event.muc)
@staticmethod
def handle_event_client_cert_passphrase(obj):
def on_ok(passphrase, checked):
obj.conn.on_client_cert_passphrase(passphrase, obj.con, obj.port,
obj.secure_tuple)
def on_cancel():
obj.conn.on_client_cert_passphrase('', obj.con, obj.port,
obj.secure_tuple)
PassphraseDialog(_('Certificate Passphrase Required'),
_('Enter the certificate passphrase for account %s') % \
obj.conn.name, ok_handler=on_ok,
cancel_handler=on_cancel)
def handle_event_password_required(self, obj):
#('PASSWORD_REQUIRED', account, None)
account = obj.conn.name
if account in self.pass_dialog:
return
text = _('Enter your password for account %s') % account
def on_ok(passphrase, save):
app.settings.set_account_setting(account, 'savepass', save)
passwords.save_password(account, passphrase, user=obj.jid)
obj.on_password(passphrase)
del self.pass_dialog[account]
def on_cancel():
del self.pass_dialog[account]
self.pass_dialog[account] = PassphraseDialog(
_('Password Required'), text, _('Save password'), ok_handler=on_ok,
cancel_handler=on_cancel)
def handle_event_roster_info(self, obj):
#('ROSTER_INFO', account, (jid, name, sub, ask, groups))
account = obj.conn.name
contacts = app.contacts.get_contacts(account, obj.jid)
if (not obj.sub or obj.sub == 'none') and \
(not obj.ask or obj.ask == 'none') and not obj.nickname and \
not obj.groups:
# contact removed us.
if contacts:
self.roster.remove_contact(obj.jid, account, backend=True)
return
elif not contacts:
if obj.sub == 'remove':
return
# Add new contact to roster
contact = app.contacts.create_contact(jid=obj.jid,
account=account, name=obj.nickname, groups=obj.groups,
show='offline', sub=obj.sub, ask=obj.ask,
avatar_sha=obj.avatar_sha)
app.contacts.add_contact(account, contact)
self.roster.add_contact(obj.jid, account)
else:
# If contact has changed (sub, ask or group) update roster
# Mind about observer status changes:
# According to xep 0162, a contact is not an observer anymore when
# we asked for auth, so also remove him if ask changed
old_groups = contacts[0].groups
if obj.sub == 'remove':
# another of our instance removed a contact. Remove it here too
self.roster.remove_contact(obj.jid, account, backend=True)
return
update = False
if contacts[0].sub != obj.sub or contacts[0].ask != obj.ask\
or old_groups != obj.groups:
# c.get_shown_groups() has changed. Reflect that in
# roster_window
self.roster.remove_contact(obj.jid, account, force=True)
update = True
for contact in contacts:
contact.name = obj.nickname or ''
contact.sub = obj.sub
contact.ask = obj.ask
contact.groups = obj.groups or []
if update:
self.roster.add_contact(obj.jid, account)
# Refilter and update old groups
for group in old_groups:
self.roster.draw_group(group, account)
self.roster.draw_contact(obj.jid, account)
if obj.jid in self.instances[account]['sub_request'] and obj.sub in (
'from', 'both'):
self.instances[account]['sub_request'][obj.jid].destroy()
def handle_event_file_send_error(self, event):
ft = self.instances['file_transfers']
ft.set_status(event.file_props, 'stop')
if helpers.allow_popup_window(event.account):
ft.show_send_error(event.file_props)
return
event = events.FileSendErrorEvent(event.file_props)
self.add_event(event.account, event.jid, event)
if helpers.allow_showing_notification(event.account):
event_type = _('File Transfer Error')
app.notification.popup(
event_type, event.jid, event.account,
'file-send-error', 'dialog-error',
event_type, event.file_props.name)
def handle_event_file_request_error(self, obj):
# ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg))
ft = self.instances['file_transfers']
ft.set_status(obj.file_props, 'stop')
errno = obj.file_props.error
if helpers.allow_popup_window(obj.conn.name):
if errno in (-4, -5):
ft.show_stopped(obj.jid, obj.file_props, obj.error_msg)
else:
ft.show_request_error(obj.file_props)
return
if errno in (-4, -5):
event_class = events.FileErrorEvent
msg_type = 'file-error'
else:
event_class = events.FileRequestErrorEvent
msg_type = 'file-request-error'
event = event_class(obj.file_props)
self.add_event(obj.conn.name, obj.jid, event)
if helpers.allow_showing_notification(obj.conn.name):
# Check if we should be notified
event_type = _('File Transfer Error')
app.notification.popup(
event_type,
obj.jid,
obj.conn.name,
msg_type,
'dialog-error',
title=event_type,
text=obj.file_props.name)
def handle_event_file_request(self, obj):
account = obj.conn.name
if obj.jid not in app.contacts.get_jid_list(account):
contact = app.contacts.create_not_in_roster_contact(
jid=obj.jid, account=account)
app.contacts.add_contact(account, contact)
self.roster.add_contact(obj.jid, account)
contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
if obj.file_props.session_type == 'jingle':
request = \
obj.stanza.getTag('jingle').getTag('content').getTag(
'description').getTag('request')
if request:
# If we get a request instead
ft_win = self.instances['file_transfers']
ft_win.add_transfer(account, contact, obj.file_props)
return
if helpers.allow_popup_window(account):
self.instances['file_transfers'].show_file_request(
account, contact, obj.file_props)
return
event = events.FileRequestEvent(obj.file_props)
self.add_event(account, obj.jid, event)
if helpers.allow_showing_notification(account):
txt = _('%s wants to send you a file.') % app.get_name_from_jid(
account, obj.jid)
event_type = _('File Transfer Request')
app.notification.popup(
event_type,
obj.jid,
account,
'file-request',
icon_name='document-send',
title=event_type,
text=txt)
@staticmethod
def handle_event_file_error(title, message):
ErrorDialog(title, message)
def handle_event_file_progress(self, account, file_props):
if time.time() - self.last_ftwindow_update > 0.5:
# Update ft window every 500ms
self.last_ftwindow_update = time.time()
self.instances['file_transfers'].set_progress(
file_props.type_, file_props.sid, file_props.received_len)
def __compare_hashes(self, account, file_props):
session = app.connections[account].get_module(
'Jingle').get_jingle_session(jid=None, sid=file_props.sid)
ft_win = self.instances['file_transfers']
h = Hashes2()
try:
file_ = open(file_props.file_name, 'rb')
except Exception:
return
hash_ = h.calculateHash(file_props.algo, file_)
file_.close()
# If the hash we received and the hash of the file are the same,
# then the file is not corrupt
jid = file_props.sender
if file_props.hash_ == hash_:
GLib.idle_add(self.popup_ft_result, account, jid, file_props)
GLib.idle_add(ft_win.set_status, file_props, 'ok')
else:
# Wrong hash, we need to get the file again!
file_props.error = -10
GLib.idle_add(self.popup_ft_result, account, jid, file_props)
GLib.idle_add(ft_win.set_status, file_props, 'hash_error')
# End jingle session
if session:
session.end_session()
def handle_event_file_rcv_completed(self, account, file_props):
ft = self.instances['file_transfers']
if file_props.error == 0:
ft.set_progress(
file_props.type_, file_props.sid, file_props.received_len)
jid = app.get_jid_without_resource(str(file_props.receiver))
app.nec.push_incoming_event(
NetworkEvent('file-transfer-completed',
file_props=file_props,
jid=jid))
else:
ft.set_status(file_props, 'stop')
if not file_props.completed and (file_props.stalled or
file_props.paused):
return
if file_props.type_ == 'r': # We receive a file
app.socks5queue.remove_receiver(file_props.sid, True, True)
if file_props.session_type == 'jingle':
if file_props.hash_ and file_props.error == 0:
# We compare hashes in a new thread
self.hashThread = Thread(target=self.__compare_hashes,
args=(account, file_props))
self.hashThread.start()
else:
# We didn't get the hash, sender probably doesn't
# support that
jid = file_props.sender
self.popup_ft_result(account, jid, file_props)
if file_props.error == 0:
ft.set_status(file_props, 'ok')
session = \
app.connections[account].get_module(
'Jingle').get_jingle_session(jid=None,
sid=file_props.sid)
# End jingle session
# TODO: Only if there are no other parallel downloads in
# this session
if session:
session.end_session()
else: # We send a file
jid = file_props.receiver
app.socks5queue.remove_sender(file_props.sid, True, True)
self.popup_ft_result(account, jid, file_props)
def popup_ft_result(self, account, jid, file_props):
ft = self.instances['file_transfers']
if helpers.allow_popup_window(account):
if file_props.error == 0:
if app.settings.get('notify_on_file_complete'):
ft.show_completed(jid, file_props)
elif file_props.error == -1:
ft.show_stopped(
jid,
file_props,
error_msg=_('Remote Contact Stopped Transfer'))
elif file_props.error == -6:
ft.show_stopped(
jid,
file_props,
error_msg=_('Error Opening File'))
elif file_props.error == -10:
ft.show_hash_error(
jid,
file_props,
account)
elif file_props.error == -12:
ft.show_stopped(
jid,
file_props,
error_msg=_('SSL Certificate Error'))
return
msg_type = ''
event_type = ''
if (file_props.error == 0 and
app.settings.get('notify_on_file_complete')):
event_class = events.FileCompletedEvent
msg_type = 'file-completed'
event_type = _('File Transfer Completed')
elif file_props.error in (-1, -6):
event_class = events.FileStoppedEvent
msg_type = 'file-stopped'
event_type = _('File Transfer Stopped')
elif file_props.error == -10:
event_class = events.FileHashErrorEvent
msg_type = 'file-hash-error'
event_type = _('File Transfer Failed')
if event_type == '':
# FIXME: ugly workaround (this can happen Gajim sent, Gaim recvs)
# this should never happen but it does. see process_result() in
# socks5.py
# who calls this func (sth is really wrong unless this func is also
# registered as progress_cb
return
if msg_type:
event = event_class(file_props)
self.add_event(account, jid, event)
if file_props is not None:
if file_props.type_ == 'r':
# Get the name of the sender, as it is in the roster
sender = file_props.sender.split('/')[0]
name = app.contacts.get_first_contact_from_jid(
account, sender).get_shown_name()
filename = os.path.basename(file_props.file_name)
if event_type == _('File Transfer Completed'):
txt = _('%(filename)s received from %(name)s.') % {
'filename': filename,
'name': name}
icon_name = 'emblem-default'
elif event_type == _('File Transfer Stopped'):
txt = _('File transfer of %(filename)s from %(name)s '
'stopped.') % {
'filename': filename,
'name': name}
icon_name = 'process-stop'
else: # File transfer hash error
txt = _('File transfer of %(filename)s from %(name)s '
'failed.') % {
'filename': filename,
'name': name}
icon_name = 'process-stop'
else:
receiver = file_props.receiver
if hasattr(receiver, 'jid'):
receiver = receiver.jid
receiver = receiver.split('/')[0]
# Get the name of the contact, as it is in the roster
name = app.contacts.get_first_contact_from_jid(
account, receiver).get_shown_name()
filename = os.path.basename(file_props.file_name)
if event_type == _('File Transfer Completed'):
txt = _('You successfully sent %(filename)s to '
'%(name)s.') % {
'filename': filename,
'name': name}
icon_name = 'emblem-default'
elif event_type == _('File Transfer Stopped'):
txt = _('File transfer of %(filename)s to %(name)s '
'stopped.') % {
'filename': filename,
'name': name}
icon_name = 'process-stop'
else: # File transfer hash error
txt = _('File transfer of %(filename)s to %(name)s '
'failed.') % {
'filename': filename,
'name': name}
icon_name = 'process-stop'
else:
txt = ''
icon_name = None
if (app.settings.get('notify_on_file_complete') and
(app.settings.get('autopopupaway') or
app.connections[account].status in ('online', 'chat'))):
# We want to be notified and we are online/chat or we don't mind
# to be bugged when away/na/busy
app.notification.popup(
event_type,
jid,
account,
msg_type,
icon_name=icon_name,
title=event_type,
text=txt)
def handle_event_signed_in(self, obj):
"""
SIGNED_IN event is emitted when we sign in, so handle it
"""
# ('SIGNED_IN', account, ())
# block signed in notifications for 30 seconds
# Add our own JID into the DB
app.storage.archive.insert_jid(obj.conn.get_own_jid().bare)
account = obj.conn.name
app.block_signed_in_notifications[account] = True
pep_supported = obj.conn.get_module('PEP').supported
if obj.conn.get_module('MAM').available:
obj.conn.get_module('MAM').request_archive_on_signin()
# enable location listener
if (pep_supported and app.is_installed('GEOCLUE') and
app.settings.get_account_setting(account, 'publish_location')):
location.enable()
if ask_for_status_message(obj.conn.status, signin=True):
open_window('StatusChange', status=obj.conn.status)
def send_httpupload(self, chat_control, path=None):
if path is not None:
self._send_httpupload(chat_control, path)
return
accept_cb = partial(self.on_file_dialog_ok, chat_control)
FileChooserDialog(accept_cb,
select_multiple=True,
transient_for=chat_control.parent_win.window)
def on_file_dialog_ok(self, chat_control, paths):
for path in paths:
self._send_httpupload(chat_control, path)
def _send_httpupload(self, chat_control, path):
con = app.connections[chat_control.account]
try:
transfer = con.get_module('HTTPUpload').make_transfer(
path,
chat_control.encryption,
chat_control.contact,
chat_control.is_groupchat)
except FileError as error:
app.nec.push_incoming_event(InformationEvent(
None, dialog_name='open-file-error2', args=error))
return
transfer.connect('cancel', self._on_cancel_upload)
transfer.connect('state-changed',
self._on_http_upload_state_changed)
FileTransferProgress(transfer)
con.get_module('HTTPUpload').start_transfer(transfer)
@staticmethod
def _on_http_upload_state_changed(transfer, _signal_name, state):
if state.is_finished:
uri = transfer.get_transformed_uri()
type_ = 'chat'
if transfer.is_groupchat:
type_ = 'groupchat'
message = OutgoingMessage(account=transfer.account,
contact=transfer.contact,
message=uri,
type_=type_,
oob_url=uri)
client = app.get_client(transfer.account)
client.send_message(message)
@staticmethod
def _on_cancel_upload(transfer, _signal_name):
client = app.get_client(transfer.account)
client.get_module('HTTPUpload').cancel_transfer(transfer)
@staticmethod
def handle_event_metacontacts(obj):
app.contacts.define_metacontacts(obj.conn.name, obj.meta_list)
def handle_event_zc_name_conflict(self, obj):
def _on_ok(new_name):
app.settings.set_account_setting(obj.conn.name, 'name', new_name)
obj.conn.username = new_name
obj.conn.change_status(obj.conn.status, obj.conn.status_message)
def _on_cancel(*args):
obj.conn.change_status('offline', '')
InputDialog(
_('Username Conflict'),
_('Username Conflict'),
_('Please enter a new username for your local account'),
[DialogButton.make('Cancel',
callback=_on_cancel),
DialogButton.make('Accept',
text=_('_OK'),
callback=_on_ok)],
input_str=obj.alt_name,
transient_for=self.roster.window).show()
def handle_event_jingleft_cancel(self, obj):
ft = self.instances['file_transfers']
file_props = None
# get the file_props of our session
file_props = FilesProp.getFileProp(obj.conn.name, obj.sid)
if not file_props:
return
ft.set_status(file_props, 'stop')
file_props.error = -4 # is it the right error code?
ft.show_stopped(obj.jid, file_props, 'Peer cancelled ' +
'the transfer')
# Jingle AV handling
def handle_event_jingle_incoming(self, event):
# ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type,
# data...))
# TODO: conditional blocking if peer is not in roster
account = event.conn.name
content_types = []
for item in event.contents:
content_types.append(item.media)
# check type of jingle session
if 'audio' in content_types or 'video' in content_types:
# a voip session...
# we now handle only voip, so the only thing we will do here is
# not to return from function
pass
else:
# unknown session type... it should be declined in common/jingle.py
return
notification_event = events.JingleIncomingEvent(
event.fjid, event.sid, content_types)
ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
or self.msg_win_mgr.get_control(event.jid, account))
if ctrl:
if 'audio' in content_types:
ctrl.set_jingle_state(
'audio',
JingleState.CONNECTION_RECEIVED,
event.sid)
if 'video' in content_types:
ctrl.set_jingle_state(
'video',
JingleState.CONNECTION_RECEIVED,
event.sid)
ctrl.add_call_received_message(notification_event)
if helpers.allow_popup_window(account):
app.interface.new_chat_from_jid(account, event.fjid)
ctrl.add_call_received_message(notification_event)
return
self.add_event(account, event.fjid, notification_event)
if helpers.allow_showing_notification(account):
heading = _('Incoming Call')
contact = app.get_name_from_jid(account, event.jid)
text = _('%s is calling') % contact
app.notification.popup(
heading,
event.fjid,
account,
'jingle-incoming',
icon_name='call-start-symbolic',
title=heading,
text=text)
def handle_event_jingle_connected(self, event):
# ('JINGLE_CONNECTED', account, (peerjid, sid, media))
if event.media in ('audio', 'video'):
account = event.conn.name
ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
or self.msg_win_mgr.get_control(event.jid, account))
if ctrl:
con = app.connections[account]
session = con.get_module('Jingle').get_jingle_session(
event.fjid, event.sid)
if event.media == 'audio':
content = session.get_content('audio')
ctrl.set_jingle_state(
'audio',
JingleState.CONNECTED,
event.sid)
if event.media == 'video':
content = session.get_content('video')
ctrl.set_jingle_state(
'video',
JingleState.CONNECTED,
event.sid)
# Now, accept the content/sessions.
# This should be done after the chat control is running
if not session.accepted:
session.approve_session()
for content in event.media:
session.approve_content(content)
def handle_event_jingle_disconnected(self, event):
# ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason))
account = event.conn.name
ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
or self.msg_win_mgr.get_control(event.jid, account))
if ctrl:
if event.media is None:
ctrl.stop_jingle(sid=event.sid, reason=event.reason)
if event.media == 'audio':
ctrl.set_jingle_state(
'audio',
JingleState.NULL,
sid=event.sid,
reason=event.reason)
if event.media == 'video':
ctrl.set_jingle_state(
'video',
JingleState.NULL,
sid=event.sid,
reason=event.reason)
def handle_event_jingle_error(self, event):
# ('JINGLE_ERROR', account, (peerjid, sid, reason))
account = event.conn.name
ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
or self.msg_win_mgr.get_control(event.jid, account))
if ctrl and event.sid == ctrl.jingle['audio'].sid:
ctrl.set_jingle_state(
'audio',
JingleState.ERROR,
reason=event.reason)
@staticmethod
def handle_event_roster_item_exchange(obj):
# data = (action in [add, delete, modify], exchange_list, jid_from)
RosterItemExchangeWindow(obj.conn.name, obj.action,
obj.exchange_items_list, obj.fjid)
def handle_event_plain_connection(self, event):
ConfirmationDialog(
_('Insecure Connection'),
_('Insecure Connection'),
_('You are about to connect to the account %(account)s '
'(%(server)s) using an insecure connection method. This means '
'conversations will not be encrypted. Connecting PLAIN is '
'strongly discouraged.') % {
'account': event.account,
'server': app.get_hostname_from_account(event.account)},
[DialogButton.make('Cancel',
text=_('_Abort'),
callback=event.abort),
DialogButton.make('Remove',
text=_('_Connect Anyway'),
callback=event.connect)]).show()
def create_core_handlers_list(self):
self.handlers = {
'DB_ERROR': [self.handle_event_db_error],
'file-send-error': [self.handle_event_file_send_error],
'client-cert-passphrase': [
self.handle_event_client_cert_passphrase],
'connection-lost': [self.handle_event_connection_lost],
'file-request-error': [self.handle_event_file_request_error],
'file-request-received': [self.handle_event_file_request],
'muc-invitation': [self.handle_event_gc_invitation],
'muc-decline': [self.handle_event_gc_decline],
'http-auth-received': [self.handle_event_http_auth],
'information': [self.handle_event_information],
'iq-error-received': [self.handle_event_iq_error],
'jingle-connected-received': [self.handle_event_jingle_connected],
'jingle-disconnected-received': [
self.handle_event_jingle_disconnected],
'jingle-error-received': [self.handle_event_jingle_error],
'jingle-request-received': [self.handle_event_jingle_incoming],
'jingle-ft-cancelled-received': [self.handle_event_jingleft_cancel],
'message-not-sent': [self.handle_event_msgnotsent],
'message-sent': [self.handle_event_msgsent],
'metacontacts-received': [self.handle_event_metacontacts],
'our-show': [self.handle_event_status],
'password-required': [self.handle_event_password_required],
'plain-connection': [self.handle_event_plain_connection],
'presence-received': [self.handle_event_presence],
'roster-info': [self.handle_event_roster_info],
'roster-item-exchange-received': \
[self.handle_event_roster_item_exchange],
'signed-in': [self.handle_event_signed_in],
'subscribe-presence-received': [
self.handle_event_subscribe_presence],
'subscribed-presence-received': [
self.handle_event_subscribed_presence],
'unsubscribed-presence-received': [
self.handle_event_unsubscribed_presence],
'zeroconf-name-conflict': [self.handle_event_zc_name_conflict],
'read-state-sync': [self.handle_event_read_state_sync],
}
def register_core_handlers(self):
"""
Register core handlers in Global Events Dispatcher (GED).
This is part of rewriting whole events handling system to use GED.
"""
for event_name, event_handlers in self.handlers.items():
for event_handler in event_handlers:
prio = ged.GUI1
if isinstance(event_handler, tuple):
prio = event_handler[1]
event_handler = event_handler[0]
app.ged.register_event_handler(event_name, prio,
event_handler)
################################################################################
### Methods dealing with app.events
################################################################################
def add_event(self, account, jid, event):
"""
Add an event to the app.events var
"""
# We add it to the app.events queue
# Do we have a queue?
jid = app.get_jid_without_resource(jid)
no_queue = len(app.events.get_events(account, jid)) == 0
# event can be in common.events.*
# event_type can be in advancedNotificationWindow.events_list
event_types = {'file-request': 'ft_request',
'file-completed': 'ft_finished'}
event_type = event_types.get(event.type_)
show_in_roster = get_show_in_roster(event_type, jid)
show_in_systray = get_show_in_systray(event_type, account, jid)
event.show_in_roster = show_in_roster
event.show_in_systray = show_in_systray
app.events.add_event(account, jid, event)
self.roster.show_title()
if no_queue: # We didn't have a queue: we change icons
if app.contacts.get_contact_with_highest_priority(account, jid):
self.roster.draw_contact(jid, account)
else:
groupchat = event.type_ == 'gc-invitation'
self.roster.add_to_not_in_the_roster(
account, jid, groupchat=groupchat)
# Select the big brother contact in roster, it's visible because it has
# events.
family = app.contacts.get_metacontacts_family(account, jid)
if family:
_nearby_family, bb_jid, bb_account = \
app.contacts.get_nearby_family_and_big_brother(family,
account)
else:
bb_jid, bb_account = jid, account
self.roster.select_contact(bb_jid, bb_account)
def handle_event(self, account, fjid, type_):
if type_ in ('connection-lost', 'connection-failed'):
app.interface.roster.window.present()
return
w = None
ctrl = None
resource = app.get_resource_from_jid(fjid)
jid = app.get_jid_without_resource(fjid)
if type_ in ('printed_gc_msg', 'printed_marked_gc_msg', 'gc_msg'):
w = self.msg_win_mgr.get_window(jid, account)
if jid in self.minimized_controls[account]:
self.roster.on_groupchat_maximized(None, jid, account)
return
ctrl = self.msg_win_mgr.get_gc_control(jid, account)
elif type_ in ('printed_chat', 'chat', ''):
# '' is for log in/out notifications
ctrl = self.msg_win_mgr.search_control(jid, account, resource)
if not ctrl:
highest_contact = app.contacts.\
get_contact_with_highest_priority(account, jid)
# jid can have a window if this resource was lower when he sent
# message and is now higher because the other one is offline
if resource and highest_contact.resource == resource and \
not self.msg_win_mgr.has_window(jid, account):
# remove resource of events too
app.events.change_jid(account, fjid, jid)
resource = None
fjid = jid
contact = None
if resource:
contact = app.contacts.get_contact(account, jid, resource)
if not contact:
contact = highest_contact
if not contact:
# Maybe we deleted the contact from the roster
return
ctrl = self.new_chat(contact, account, resource=resource)
app.last_message_time[account][jid] = 0 # long time ago
w = ctrl.parent_win
elif type_ in ('printed_pm', 'pm'):
ctrl = self.msg_win_mgr.get_control(fjid, account)
if not ctrl:
room_jid = jid
nick = resource
gc_contact = app.contacts.get_gc_contact(
account, room_jid, nick)
ctrl = self.new_private_chat(gc_contact, account)
w = ctrl.parent_win
elif type_ in ('file-request', 'file-request-error',
'file-send-error', 'file-error', 'file-stopped', 'file-completed',
'file-hash-error', 'jingle-incoming'):
# Get the first single message event
event = app.events.get_first_event(account, fjid, type_)
if not event:
# default to jid without resource
event = app.events.get_first_event(account, jid, type_)
if not event:
return
# Open the window
self.roster.open_event(account, jid, event)
else:
# Open the window
self.roster.open_event(account, fjid, event)
elif type_ == 'gc-invitation':
event = app.events.get_first_event(account, jid, type_)
if event is None:
return
open_window('GroupChatInvitation',
account=account,
event=event)
app.events.remove_events(account, jid, event)
self.roster.draw_contact(jid, account)
elif type_ == 'subscription_request':
event = app.events.get_first_event(account, jid, type_)
if event is None:
return
open_window('SubscriptionRequest',
account=account,
jid=jid,
text=event.text,
user_nick=event.nick)
app.events.remove_events(account, jid, event)
self.roster.draw_contact(jid, account)
elif type_ == 'unsubscribed':
event = app.events.get_first_event(account, jid, type_)
if event is None:
return
self.show_unsubscribed_dialog(account, event.contact)
app.events.remove_events(account, jid, event)
self.roster.draw_contact(jid, account)
if w:
w.set_active_tab(ctrl)
w.window.present()
# Using isinstance here because we want to catch all derived types
if isinstance(ctrl, ChatControlBase):
ctrl.scroll_to_end()
################################################################################
### Methods for opening new messages controls
################################################################################
def show_groupchat(self, account, room_jid):
minimized_control = self.minimized_controls[account].get(room_jid)
if minimized_control is not None:
self.roster.on_groupchat_maximized(None, room_jid, account)
return True
if self.msg_win_mgr.has_window(room_jid, account):
gc_ctrl = self.msg_win_mgr.get_gc_control(room_jid, account)
# FIXME: Access message window directly
gc_ctrl.parent_win.set_active_tab(gc_ctrl)
return True
return False
def create_groupchat_control(self, account, room_jid, muc_data,
minimize=False):
avatar_sha = app.storage.cache.get_muc_avatar_sha(room_jid)
contact = app.contacts.create_contact(jid=room_jid,
account=account,
groups=[_('Group chats')],
sub='none',
avatar_sha=avatar_sha,
groupchat=True)
app.contacts.add_contact(account, contact)
if minimize:
control = GroupchatControl(None, contact, muc_data, account)
app.interface.minimized_controls[account][room_jid] = control
self.roster.add_groupchat(room_jid, account)
else:
mw = self.msg_win_mgr.get_window(room_jid, account)
if not mw:
mw = self.msg_win_mgr.create_window(contact,
account,
ControlType.GROUPCHAT)
control = GroupchatControl(mw, contact, muc_data, account)
mw.new_tab(control)
mw.set_active_tab(control)
@staticmethod
def _create_muc_data(account, room_jid, nick, password, config):
if not nick:
nick = get_group_chat_nick(account, room_jid)
# Fetch data from bookmarks
client = app.get_client(account)
bookmark = client.get_module('Bookmarks').get_bookmark(room_jid)
if bookmark is not None:
if bookmark.password is not None:
password = bookmark.password
return MUCData(room_jid, nick, password, config)
def create_groupchat(self, account, room_jid, config=None):
muc_data = self._create_muc_data(account, room_jid, None, None, config)
self.create_groupchat_control(account, room_jid, muc_data)
app.connections[account].get_module('MUC').create(muc_data)
def show_or_join_groupchat(self, account, room_jid, **kwargs):
if self.show_groupchat(account, room_jid):
return
self.join_groupchat(account, room_jid, **kwargs)
def join_groupchat(self,
account,
room_jid,
password=None,
nick=None,
minimized=False):
if not app.account_is_available(account):
return
muc_data = self._create_muc_data(account,
room_jid,
nick,
password,
None)
self.create_groupchat_control(
account, room_jid, muc_data, minimize=minimized)
app.connections[account].get_module('MUC').join(muc_data)
def new_private_chat(self, gc_contact, account, session=None):
conn = app.connections[account]
if not session and gc_contact.get_full_jid() in conn.sessions:
sessions = [s for s in conn.sessions[gc_contact.get_full_jid()].\
values() if isinstance(s, ChatControlSession)]
# look for an existing session with a chat control
for s in sessions:
if s.control:
session = s
break
if not session and sessions:
# there are no sessions with chat controls, just take the first
# one
session = sessions[0]
if not session:
# couldn't find an existing ChatControlSession, just make a new one
session = conn.make_new_session(gc_contact.get_full_jid(), None,
'pm')
contact = gc_contact.as_contact()
if not session.control:
message_window = self.msg_win_mgr.get_window(
gc_contact.get_full_jid(), account)
if not message_window:
message_window = self.msg_win_mgr.create_window(
contact, account, ControlType.PRIVATECHAT)
session.control = PrivateChatControl(message_window, gc_contact,
contact, account, session)
message_window.new_tab(session.control)
if app.events.get_events(account, gc_contact.get_full_jid()):
# We call this here to avoid race conditions with widget validation
session.control.read_queue()
return session.control
def new_chat(self, contact, account, resource=None, session=None):
# Get target window, create a control, and associate it with the window
fjid = contact.jid
if resource:
fjid += '/' + resource
mw = self.msg_win_mgr.get_window(fjid, account)
if not mw:
mw = self.msg_win_mgr.create_window(
contact, account, ControlType.CHAT, resource)
chat_control = ChatControl(mw, contact, account, session, resource)
mw.new_tab(chat_control)
if app.events.get_events(account, fjid):
# We call this here to avoid race conditions with widget validation
chat_control.read_queue()
return chat_control
def new_chat_from_jid(self, account, fjid, message=None):
jid, resource = app.get_room_and_nick_from_fjid(fjid)
contact = app.contacts.get_contact(account, jid, resource)
added_to_roster = False
if not contact:
added_to_roster = True
contact = self.roster.add_to_not_in_the_roster(account, jid,
resource=resource)
ctrl = self.msg_win_mgr.get_control(fjid, account)
if not ctrl:
ctrl = self.new_chat(contact, account,
resource=resource)
if app.events.get_events(account, fjid):
ctrl.read_queue()
if message:
buffer_ = ctrl.msg_textview.get_buffer()
buffer_.set_text(message)
mw = ctrl.parent_win
mw.set_active_tab(ctrl)
# For JEP-0172
if added_to_roster:
ctrl.user_nick = app.nicks[account]
return ctrl
def on_open_chat_window(self, widget, contact, account, resource=None,
session=None):
# Get the window containing the chat
fjid = contact.jid
if resource:
fjid += '/' + resource
ctrl = None
if session:
ctrl = session.control
if not ctrl:
win = self.msg_win_mgr.get_window(fjid, account)
if win:
ctrl = win.get_control(fjid, account)
if not ctrl:
ctrl = self.new_chat(contact, account, resource=resource,
session=session)
# last message is long time ago
app.last_message_time[account][ctrl.get_full_jid()] = 0
win = ctrl.parent_win
win.set_active_tab(ctrl)
if app.connections[account].is_zeroconf and \
app.connections[account].status == 'offline':
ctrl = win.get_control(fjid, account)
if ctrl:
ctrl.got_disconnected()
################################################################################
### Other Methods
################################################################################
@staticmethod
def create_account(account,
username,
domain,
password,
proxy_name,
custom_host,
anonymous=False):
account_label = f'{username}@{domain}'
if anonymous:
username = 'anon'
account_label = f'anon@{domain}'
config = {}
config['active'] = False
config['name'] = username
config['resource'] = 'gajim.%s' % helpers.get_random_string(8)
config['account_label'] = account_label
config['account_color'] = get_color_for_account(
'%s@%s' % (username, domain))
config['hostname'] = domain
config['savepass'] = True
config['anonymous_auth'] = anonymous
config['autoconnect'] = True
config['sync_with_global_status'] = True
if proxy_name is not None:
config['proxy'] = proxy_name
use_custom_host = custom_host is not None
config['use_custom_host'] = use_custom_host
if custom_host:
host, _protocol, type_ = custom_host
host, port = host.rsplit(':', maxsplit=1)
config['custom_port'] = int(port)
config['custom_host'] = host
config['custom_type'] = type_.value
app.settings.add_account(account)
for opt in config:
app.settings.set_account_setting(account, opt, config[opt])
# Password module depends on existing config
passwords.save_password(account, password, user=account_label)
app.css_config.refresh()
# Action must be added before account window is updated
app.app.add_account_actions(account)
window = get_app_window('AccountsWindow')
if window is not None:
window.add_account(account)
def enable_account(self, account):
if account == app.ZEROCONF_ACC_NAME:
app.connections[account] = connection_zeroconf.ConnectionZeroconf(
account)
else:
app.connections[account] = Client(account)
app.plugin_manager.register_modules_for_account(
app.connections[account])
# update variables
self.instances[account] = {
'infos': {}, 'disco': {}, 'gc_config': {}, 'search': {},
'sub_request': {}}
self.minimized_controls[account] = {}
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.settings.get_account_setting(account,
'name')
app.block_signed_in_notifications[account] = True
app.last_message_time[account] = {}
# refresh roster
if len(app.connections) >= 2:
# Do not merge accounts if only one exists
self.roster.regroup = app.settings.get('mergeaccounts')
else:
self.roster.regroup = False
self.roster.setup_and_draw_roster()
gui_menu_builder.build_accounts_menu()
self.roster.send_status(account, 'online', '')
app.settings.set_account_setting(account, 'active', True)
app.app.update_app_actions_state()
window = get_app_window('AccountsWindow')
if window is not None:
GLib.idle_add(window.enable_account, account, True)
def disable_account(self, account):
self.roster.close_all(account, force=True)
for jid in self.minimized_controls[account]:
ctrl = self.minimized_controls[account][jid]
ctrl.shutdown()
for win in get_app_windows(account):
# Close all account specific windows, except the RemoveAccount
# dialog. It shows if the removal was successful.
if type(win).__name__ == 'RemoveAccount':
continue
win.destroy()
if account == app.ZEROCONF_ACC_NAME:
app.connections[account].disable_account()
app.connections[account].cleanup()
del app.connections[account]
del self.instances[account]
del self.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.last_message_time[account]
if len(app.connections) >= 2:
# Do not merge accounts if only one exists
self.roster.regroup = app.settings.get('mergeaccounts')
else:
self.roster.regroup = False
app.settings.set_account_setting(account, 'roster_version', '')
self.roster.setup_and_draw_roster()
self.roster.update_status_selector()
gui_menu_builder.build_accounts_menu()
app.settings.set_account_setting(account, 'active', False)
app.app.update_app_actions_state()
def remove_account(self, account):
if app.settings.get_account_setting(account, 'active'):
self.disable_account(account)
app.storage.cache.remove_roster(account)
# Delete password must be before del_per() because it calls set_per()
# which would recreate the account with defaults values if not found
passwords.delete_password(account)
app.settings.remove_account(account)
app.app.remove_account_actions(account)
window = get_app_window('AccountsWindow')
if window is not None:
window.remove_account(account)
def autoconnect(self):
"""
Auto connect at startup
"""
for account in app.connections:
if not app.settings.get_account_setting(account, 'autoconnect'):
continue
status = 'online'
status_message = ''
if app.settings.get_account_setting(account, 'restore_last_status'):
status = app.settings.get_account_setting(account, 'last_status')
status_message = app.settings.get_account_setting(
account, 'last_status_msg')
status_message = helpers.from_one_line(status_message)
self.roster.send_status(account, status, status_message)
def change_status(self, status=None):
# status=None means we want to change the message only
ask = ask_for_status_message(status)
if status is None:
status = helpers.get_global_show()
if ask:
open_window('StatusChange', status=status)
return
for account in app.connections:
if not app.settings.get_account_setting(account,
'sync_with_global_status'):
continue
message = app.get_client(account).status_message
self.roster.send_status(account, status, message)
def change_account_status(self, account, status=None):
# status=None means we want to change the message only
ask = ask_for_status_message(status)
client = app.get_client(account)
if status is None:
status = client.status
if ask:
open_window('StatusChange', status=status, account=account)
return
message = client.status_message
self.roster.send_status(account, status, message)
def show_systray(self):
if not app.is_display(Display.WAYLAND):
self.systray_enabled = True
self.systray.show_icon()
def hide_systray(self):
if not app.is_display(Display.WAYLAND):
self.systray_enabled = False
self.systray.hide_icon()
def process_connections(self):
"""
Called each foo (200) milliseconds. Check for idlequeue timeouts
"""
try:
app.idlequeue.process()
except Exception:
# Otherwise, an exception will stop our loop
if sys.platform == 'win32':
# On Windows process() calls select.select(), so we need this
# executed as often as possible.
# Adding it directly with GLib.idle_add() causes Gajim to use
# too much CPU time. That's why its added with 1ms timeout.
# On Linux only alarms are checked in process(), so we use
# a bigger timeout
timeout, in_seconds = 1, None
else:
timeout, in_seconds = app.idlequeue.PROCESS_TIMEOUT
if in_seconds:
GLib.timeout_add_seconds(timeout, self.process_connections)
else:
GLib.timeout_add(timeout, self.process_connections)
raise
return True # renew timeout (loop for ever)
@staticmethod
def save_config():
app.settings.save()
def update_avatar(self, account=None, jid=None,
contact=None, room_avatar=False):
self.avatar_storage.invalidate_cache(jid or contact.get_full_jid())
if room_avatar:
app.nec.push_incoming_event(
NetworkEvent('update-room-avatar', account=account, jid=jid))
elif contact is None:
app.nec.push_incoming_event(
NetworkEvent('update-roster-avatar', account=account, jid=jid))
else:
app.nec.push_incoming_event(NetworkEvent('update-gc-avatar',
contact=contact,
room_jid=contact.room_jid))
def save_avatar(self, data):
return self.avatar_storage.save_avatar(data)
def get_avatar(self, contact, size, scale, show=None, pixbuf=False):
if pixbuf:
return self.avatar_storage.get_pixbuf(contact, size, scale, show)
return self.avatar_storage.get_surface(contact, size, scale, show)
def avatar_exists(self, filename):
return self.avatar_storage.get_avatar_path(filename) is not None
# does JID exist only within a groupchat?
def is_pm_contact(self, fjid, account):
bare_jid = app.get_jid_without_resource(fjid)
gc_ctrl = self.msg_win_mgr.get_gc_control(bare_jid, account)
if not gc_ctrl and \
bare_jid in self.minimized_controls[account]:
gc_ctrl = self.minimized_controls[account][bare_jid]
return gc_ctrl and gc_ctrl.is_groupchat
@staticmethod
def create_ipython_window():
# Check if IPython is installed
ipython = find_spec('IPython')
is_installed = ipython is not None
if not is_installed:
# Abort early to avoid tracebacks
print('IPython is not installed')
return
try:
from gajim.dev.ipython_view import IPythonView
except ImportError:
print('ipython_view not found')
return
from gi.repository import Pango
if os.name == 'nt':
font = 'Lucida Console 9'
else:
font = 'Luxi Mono 10'
window = Gtk.Window()
window.set_title(_('Gajim: IPython Console'))
window.set_size_request(750, 550)
window.set_resizable(True)
sw = Gtk.ScrolledWindow()
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
view = IPythonView()
view.override_font(Pango.FontDescription(font))
view.set_wrap_mode(Gtk.WrapMode.CHAR)
sw.add(view)
window.add(sw)
window.show_all()
def on_delete(win, event):
win.hide()
return True
window.connect('delete_event', on_delete)
view.updateNamespace({'gajim': app})
app.ipython_window = window
def _network_status_changed(self, monitor, _param):
connected = monitor.get_network_available()
if connected == self.network_state:
return
self.network_state = connected
if connected:
log.info('Network connection available')
else:
log.info('Network connection lost')
for connection in app.connections.values():
if (connection.state.is_connected or
connection.state.is_available):
connection.disconnect(gracefully=False, reconnect=True)
def create_zeroconf_default_config(self):
if app.settings.get_account_setting(app.ZEROCONF_ACC_NAME, 'name'):
return
log.info('Creating zeroconf account')
app.settings.add_account(app.ZEROCONF_ACC_NAME)
app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
'autoconnect',
True)
app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
'no_log_for',
'')
app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
'password',
'zeroconf')
app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
'sync_with_global_status',
True)
app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
'custom_port',
5298)
app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
'is_zeroconf',
True)
app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
'use_ft_proxies',
False)
app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
'active',
False)
def check_for_updates(self):
if not app.settings.get('check_for_update'):
return
now = datetime.now()
last_check = app.settings.get('last_update_check')
if not last_check:
def _on_cancel():
app.settings.set('check_for_update', False)
def _on_check():
self._get_latest_release()
ConfirmationDialog(
_('Update Check'),
_('Gajim Update Check'),
_('Search for Gajim updates periodically?'),
[DialogButton.make('Cancel',
text=_('_No'),
callback=_on_cancel),
DialogButton.make('Accept',
text=_('_Search Periodically'),
callback=_on_check)]).show()
return
last_check_time = datetime.strptime(last_check, '%Y-%m-%d %H:%M')
if (now - last_check_time).days < 7:
return
self._get_latest_release()
def _get_latest_release(self):
log.info('Checking for Gajim updates')
session = Soup.Session()
session.props.user_agent = 'Gajim %s' % app.version
message = Soup.Message.new('GET', 'https://gajim.org/current-version.json')
session.queue_message(message, self._on_update_checked)
def _on_update_checked(self, _session, message):
now = datetime.now()
app.settings.set('last_update_check', now.strftime('%Y-%m-%d %H:%M'))
body = message.props.response_body.data
if not body:
log.warning('Could not reach gajim.org for update check')
return
data = json.loads(body)
latest_version = data['current_version']
if V(latest_version) > V(app.version):
def _on_cancel(is_checked):
if is_checked:
app.settings.set('check_for_update', False)
def _on_update(is_checked):
if is_checked:
app.settings.set('check_for_update', False)
helpers.open_uri('https://gajim.org/download')
ConfirmationCheckDialog(
_('Update Available'),
_('Gajim Update Available'),
_('There is an update available for Gajim '
'(latest version: %s)') % str(latest_version),
_('_Do not show again'),
[DialogButton.make('Cancel',
text=_('_Later'),
callback=_on_cancel),
DialogButton.make('Accept',
text=_('_Update Now'),
callback=_on_update)]).show()
else:
log.info('Gajim is up to date')
def run(self, application):
if app.settings.get('trayicon') != 'never':
self.show_systray()
self.roster = roster_window.RosterWindow(application)
if self.msg_win_mgr.mode == \
MessageWindowMgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER:
self.msg_win_mgr.create_window(None, None, None)
# Creating plugin manager
from gajim import plugins
app.plugin_manager = plugins.PluginManager()
app.plugin_manager.init_plugins()
self.roster._before_fill()
for account in app.connections:
app.connections[account].get_module('Roster').load_roster()
self.roster._after_fill()
# get instances for windows/dialogs that will show_all()/hide()
self.instances['file_transfers'] = FileTransfersWindow()
GLib.timeout_add(100, self.autoconnect)
if sys.platform == 'win32':
timeout, in_seconds = 20, None
else:
timeout, in_seconds = app.idlequeue.PROCESS_TIMEOUT
if in_seconds:
GLib.timeout_add_seconds(timeout, self.process_connections)
else:
GLib.timeout_add(timeout, self.process_connections)
def remote_init():
if app.settings.get('remote_control'):
try:
from gajim import remote_control
remote_control.GajimRemote()
except Exception:
pass
GLib.timeout_add_seconds(5, remote_init)
def __init__(self):
app.interface = self
app.thread_interface = ThreadInterface
# This is the manager and factory of message windows set by the module
self.msg_win_mgr = None
self.minimized_controls = {}
self.pass_dialog = {}
self.db_error_dialog = None
self.handlers = {}
self.roster = None
self.avatar_storage = AvatarStorage()
# Load CSS files
app.load_css_config()
app.storage.archive.reset_shown_unread_messages()
for account in app.settings.get_accounts():
if app.settings.get_account_setting(account, 'is_zeroconf'):
app.ZEROCONF_ACC_NAME = account
break
app.idlequeue = idlequeue.get_idlequeue()
# resolve and keep current record of resolved hosts
app.socks5queue = socks5.SocksQueue(app.idlequeue,
self.handle_event_file_rcv_completed,
self.handle_event_file_progress,
self.handle_event_file_error)
app.proxy65_manager = proxy65_manager.Proxy65Manager(app.idlequeue)
app.default_session_type = ChatControlSession
# Creating Network Events Controller
from gajim.common import nec
app.nec = nec.NetworkEventsController()
app.notification = Notification()
self.create_core_handlers_list()
self.register_core_handlers()
# self.create_zeroconf_default_config()
# if app.settings.get_account_setting(app.ZEROCONF_ACC_NAME, 'active') \
# and app.is_installed('ZEROCONF'):
# app.connections[app.ZEROCONF_ACC_NAME] = \
# connection_zeroconf.ConnectionZeroconf(app.ZEROCONF_ACC_NAME)
for account in app.settings.get_accounts():
if (not app.settings.get_account_setting(account, 'is_zeroconf') and
app.settings.get_account_setting(account, 'active')):
app.connections[account] = Client(account)
self.instances = {}
for a in app.connections:
self.instances[a] = {'infos': {}, 'disco': {}, 'gc_config': {},
'search': {}, 'sub_request': {}}
self.minimized_controls[a] = {}
app.contacts.add_account(a)
app.groups[a] = {}
app.gc_connected[a] = {}
app.automatic_rooms[a] = {}
app.newly_added[a] = []
app.to_be_removed[a] = []
app.nicks[a] = app.settings.get_account_setting(a, 'name')
app.block_signed_in_notifications[a] = True
app.last_message_time[a] = {}
if sys.platform not in ('win32', 'darwin'):
logind.enable()
music_track.enable()
else:
GLib.timeout_add_seconds(20, self.check_for_updates)
idle.Monitor.set_interval(app.settings.get('autoawaytime') * 60,
app.settings.get('autoxatime') * 60)
self.systray_enabled = False
if not app.is_display(Display.WAYLAND):
from gajim.gui import statusicon
self.systray = statusicon.StatusIcon()
if sys.platform in ('win32', 'darwin'):
from gajim.gui.emoji_chooser import emoji_chooser
emoji_chooser.load()
self.last_ftwindow_update = 0
self._network_monitor = Gio.NetworkMonitor.get_default()
self._network_monitor.connect('notify::network-available',
self._network_status_changed)
self.network_state = self._network_monitor.get_network_available()
class ThreadInterface:
def __init__(self, func, func_args=(), callback=None, callback_args=()):
"""
Call a function in a thread
"""
def thread_function(func, func_args, callback, callback_args):
output = func(*func_args)
if callback:
GLib.idle_add(callback, output, *callback_args)
Thread(target=thread_function, args=(func, func_args, callback,
callback_args)).start()