- There is a PrepareForSleep signal before and after every sleep event. The expectation is that before sleep, gajim cleans up and then releases the sleep inhibitor. Accordingly, we must reacquire the inhibitor after sleeping, which can helpfully be done with the other signal. - Moved module to the dbus folder - Embed logic in a LogindListener object so we can import the module without listening to dbus
2877 lines
120 KiB
Python
2877 lines
120 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 re
|
|
import time
|
|
import hashlib
|
|
import logging
|
|
from functools import partial
|
|
from threading import Thread
|
|
|
|
from gi.repository import Gtk
|
|
from gi.repository import GdkPixbuf
|
|
from gi.repository import GLib
|
|
from gi.repository import Gio
|
|
from gi.repository import Gdk
|
|
from nbxmpp import idlequeue
|
|
from nbxmpp import Hashes2
|
|
import OpenSSL
|
|
|
|
try:
|
|
from PIL import Image
|
|
except Exception:
|
|
pass
|
|
|
|
from gajim.common import app
|
|
from gajim.common import events
|
|
from gajim.common.dbus import screensaver
|
|
from gajim.common.dbus import location
|
|
from gajim.common.dbus import music_track
|
|
from gajim.common.dbus import logind
|
|
|
|
from gajim import gtkgui_helpers
|
|
from gajim import gui_menu_builder
|
|
from gajim import dialogs
|
|
from gajim import notify
|
|
from gajim import message_control
|
|
from gajim.dialog_messages import get_dialog
|
|
from gajim.dialogs import ProgressWindow
|
|
|
|
from gajim.chat_control_base import ChatControlBase
|
|
from gajim.chat_control import ChatControl
|
|
from gajim.groupchat_control import GroupchatControl
|
|
from gajim.groupchat_control import PrivateChatControl
|
|
from gajim.message_window import MessageWindowMgr
|
|
from gajim.filetransfers_window import FileTransfersWindow
|
|
|
|
from gajim.atom_window import AtomWindow
|
|
from gajim.session import ChatControlSession
|
|
|
|
from gajim.common import idle
|
|
from gajim.common.zeroconf import connection_zeroconf
|
|
from gajim.common import resolver
|
|
from gajim.common import caps_cache
|
|
from gajim.common import proxy65_manager
|
|
from gajim.common import socks5
|
|
from gajim.common import helpers
|
|
from gajim.common import passwords
|
|
from gajim.common import logging_helpers
|
|
from gajim.common.i18n import _
|
|
from gajim.common.connection_handlers_events import (
|
|
OurShowEvent, FileRequestErrorEvent, FileTransferCompletedEvent,
|
|
UpdateRosterAvatarEvent, UpdateGCAvatarEvent, UpdateRoomAvatarEvent)
|
|
|
|
from gajim.common.modules.httpupload import HTTPUploadProgressEvent
|
|
from gajim.common.connection import Connection
|
|
from gajim.common.file_props import FilesProp
|
|
from gajim.common.const import AvatarSize, SSLError, PEPEventType
|
|
from gajim.common.const import ACTIVITIES, MOODS
|
|
|
|
from gajim import roster_window
|
|
from gajim import config
|
|
from gajim.common import ged
|
|
from gajim.common.caps_cache import muc_caps_cache
|
|
from gajim.common import configpaths
|
|
from gajim.common import optparser
|
|
|
|
from gajim.gtk.dialogs import ErrorDialog
|
|
from gajim.gtk.dialogs import WarningDialog
|
|
from gajim.gtk.dialogs import InformationDialog
|
|
from gajim.gtk.dialogs import InputDialog
|
|
from gajim.gtk.dialogs import YesNoDialog
|
|
from gajim.gtk.dialogs import InputTextDialog
|
|
from gajim.gtk.dialogs import PlainConnectionDialog
|
|
from gajim.gtk.dialogs import SSLErrorDialog
|
|
from gajim.gtk.dialogs import ConfirmationDialogDoubleCheck
|
|
from gajim.gtk.dialogs import ChangeNickDialog
|
|
from gajim.gtk.profile import ProfileWindow
|
|
from gajim.gtk.join_groupchat import JoinGroupchatWindow
|
|
from gajim.gtk.filechoosers import FileChooserDialog
|
|
from gajim.gtk.emoji_data import emoji_data
|
|
from gajim.gtk.emoji_data import emoji_ascii_data
|
|
|
|
|
|
parser = optparser.OptionsParser(configpaths.get('CONFIG_FILE'))
|
|
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)
|
|
|
|
def handle_ask_new_nick(self, account, room_jid, parent_win):
|
|
title = _('Unable to join group chat')
|
|
prompt = _('Your desired nickname in group chat\n'
|
|
'<b>%s</b>\n'
|
|
'is in use or registered by another occupant.\n'
|
|
'Please specify another nickname below:') % room_jid
|
|
if 'change_nick_dialog' in self.instances:
|
|
self.instances['change_nick_dialog'].add_room(
|
|
account, room_jid, prompt)
|
|
else:
|
|
self.instances['change_nick_dialog'] = ChangeNickDialog(
|
|
account, room_jid, title, prompt, transient_for=parent_win)
|
|
|
|
@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)
|
|
|
|
def on_yes(is_checked, obj):
|
|
response(obj, 'yes')
|
|
|
|
account = obj.conn.name
|
|
sec_msg = _('Do you accept this request?')
|
|
if app.get_number_of_connected_accounts() > 1:
|
|
sec_msg = _('Do you accept this request on account %s?') % account
|
|
if obj.msg:
|
|
sec_msg = obj.msg + '\n' + sec_msg
|
|
YesNoDialog(_('HTTP (%(method)s) Authorization for '
|
|
'%(url)s (ID: %(id)s)') % {'method': obj.method, 'url': obj.url,
|
|
'id': obj.iq_id}, sec_msg, on_response_yes=(on_yes, obj),
|
|
on_response_no=(response, obj, 'no'))
|
|
|
|
def handle_event_iq_error(self, obj):
|
|
#('ERROR_ANSWER', account, (id_, fjid, errmsg, errcode))
|
|
if str(obj.errcode) in ('400', '403', '406') and obj.id_:
|
|
# show the error dialog
|
|
sid = obj.id_
|
|
if len(obj.id_) > 3 and obj.id_[2] == '_':
|
|
sid = obj.id_[3:]
|
|
file_props = FilesProp.getFileProp(obj.conn.name, sid)
|
|
if file_props:
|
|
if str(obj.errcode) == '400':
|
|
file_props.error = -3
|
|
else:
|
|
file_props.error = -4
|
|
app.nec.push_incoming_event(FileRequestErrorEvent(None,
|
|
conn=obj.conn, jid=obj.jid, file_props=file_props,
|
|
error_msg=obj.errmsg))
|
|
obj.conn.disconnect_transfer(file_props)
|
|
return
|
|
elif str(obj.errcode) == '404':
|
|
sid = obj.id_
|
|
if len(obj.id_) > 3 and obj.id_[2] == '_':
|
|
sid = obj.id_[3:]
|
|
file_props = FilesProp.getFileProp(obj.conn.name, sid)
|
|
if file_props:
|
|
self.handle_event_file_send_error(obj.conn.name, (obj.fjid,
|
|
file_props))
|
|
obj.conn.disconnect_transfer(file_props)
|
|
return
|
|
|
|
ctrl = self.msg_win_mgr.get_control(obj.fjid, obj.conn.name)
|
|
if ctrl and ctrl.type_id == message_control.TYPE_GC:
|
|
ctrl.print_conversation('Error %s: %s' % (obj.errcode, obj.errmsg))
|
|
|
|
@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, obj): # OUR status
|
|
#('STATUS', account, show)
|
|
account = obj.conn.name
|
|
if obj.show in ('offline', 'error'):
|
|
for name in list(self.instances[account]['online_dialog'].keys()):
|
|
# .keys() is needed to not have a dictionary length changed
|
|
# during iteration error
|
|
self.instances[account]['online_dialog'][name].destroy()
|
|
if name in self.instances[account]['online_dialog']:
|
|
# destroy handler may have already removed it
|
|
del self.instances[account]['online_dialog'][name]
|
|
for request in self.gpg_passphrase.values():
|
|
if request:
|
|
request.interrupt(account=account)
|
|
if account in self.pass_dialog:
|
|
self.pass_dialog[account].window.destroy()
|
|
if obj.show == 'offline':
|
|
app.block_signed_in_notifications[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,
|
|
account)
|
|
|
|
if account in self.show_vcard_when_connect and obj.show not in (
|
|
'offline', 'error'):
|
|
self.edit_own_details(account)
|
|
self.show_vcard_when_connect.remove(account)
|
|
|
|
def edit_own_details(self, account):
|
|
window = app.get_app_window(ProfileWindow)
|
|
if window is None:
|
|
ProfileWindow(account)
|
|
else:
|
|
window.present()
|
|
|
|
@staticmethod
|
|
def handle_gc_error(gc_control, pritext, sectext):
|
|
if gc_control and gc_control.autorejoin is not None:
|
|
if gc_control.error_dialog:
|
|
gc_control.error_dialog.destroy()
|
|
def on_close(dummy):
|
|
gc_control.error_dialog.destroy()
|
|
gc_control.error_dialog = None
|
|
gc_control.error_dialog = ErrorDialog(pritext, sectext,
|
|
on_response_ok=on_close, on_response_cancel=on_close)
|
|
gc_control.error_dialog.set_modal(False)
|
|
if gc_control.parent_win:
|
|
gc_control.error_dialog.set_transient_for(
|
|
gc_control.parent_win.window)
|
|
else:
|
|
d = ErrorDialog(pritext, sectext)
|
|
if gc_control and gc_control.parent_win:
|
|
d.set_transient_for(gc_control.parent_win.window)
|
|
d.set_modal(False)
|
|
|
|
def handle_gc_password_required(self, account, room_jid, nick):
|
|
def on_ok(text):
|
|
app.connections[account].join_gc(nick, room_jid, text)
|
|
app.gc_passwords[room_jid] = text
|
|
gc_control.error_dialog = None
|
|
|
|
def on_cancel():
|
|
# get and destroy window
|
|
if room_jid in app.interface.minimized_controls[account]:
|
|
self.roster.on_disconnect(None, room_jid, account)
|
|
else:
|
|
win = self.msg_win_mgr.get_window(room_jid, account)
|
|
ctrl = self.msg_win_mgr.get_gc_control(room_jid, account)
|
|
win.remove_tab(ctrl, 3)
|
|
gc_control.error_dialog = None
|
|
|
|
gc_control = self.msg_win_mgr.get_gc_control(room_jid, account)
|
|
if gc_control:
|
|
if gc_control.error_dialog:
|
|
gc_control.error_dialog.destroy()
|
|
|
|
gc_control.error_dialog = InputDialog(_('Password Required'),
|
|
_('A Password is required to join the room %s. Please type it.') % \
|
|
room_jid, is_modal=False, ok_handler=on_ok,
|
|
cancel_handler=on_cancel)
|
|
gc_control.error_dialog.input_entry.set_visibility(False)
|
|
|
|
def handle_event_gc_presence(self, obj):
|
|
gc_control = obj.gc_control
|
|
parent_win = None
|
|
if gc_control and gc_control.parent_win:
|
|
parent_win = gc_control.parent_win.window
|
|
if obj.ptype == 'error':
|
|
if obj.errcode == '503':
|
|
# maximum user number reached
|
|
self.handle_gc_error(gc_control,
|
|
_('Unable to join group chat'),
|
|
_('<b>%s</b> is full')\
|
|
% obj.room_jid)
|
|
elif (obj.errcode == '401') or (obj.errcon == 'not-authorized'):
|
|
# password required to join
|
|
self.handle_gc_password_required(obj.conn.name, obj.room_jid,
|
|
obj.nick)
|
|
elif (obj.errcode == '403') or (obj.errcon == 'forbidden'):
|
|
# we are banned
|
|
self.handle_gc_error(gc_control, _('Unable to join group chat'),
|
|
_('You are banned from group chat <b>%s</b>.') % \
|
|
obj.room_jid)
|
|
elif (obj.errcode == '404') or (obj.errcon in ('item-not-found',
|
|
'remote-server-not-found')):
|
|
# remote server does not exist
|
|
if obj.errcon == 'remote-server-not-found':
|
|
self.handle_gc_error(gc_control, _('Unable to join group chat'),
|
|
_('Remote server <b>%s</b> does not exist.') % obj.room_jid)
|
|
# group chat does not exist
|
|
else:
|
|
self.handle_gc_error(gc_control, _('Unable to join group chat'),
|
|
_('Group chat <b>%s</b> does not exist.') % obj.room_jid)
|
|
elif (obj.errcode == '405') or (obj.errcon == 'not-allowed'):
|
|
self.handle_gc_error(gc_control, _('Unable to join group chat'),
|
|
_('Group chat creation is not permitted.'))
|
|
elif (obj.errcode == '406') or (obj.errcon == 'not-acceptable'):
|
|
self.handle_gc_error(gc_control, _('Unable to join groupchat'),
|
|
_('You must use your registered nickname in <b>%s</b>.')\
|
|
% obj.room_jid)
|
|
elif (obj.errcode == '407') or (obj.errcon == \
|
|
'registration-required'):
|
|
self.handle_gc_error(gc_control, _('Unable to join group chat'),
|
|
_('You are not in the members list in groupchat %s.') % \
|
|
obj.room_jid)
|
|
elif (obj.errcode == '409') or (obj.errcon == 'conflict'):
|
|
self.handle_ask_new_nick(obj.conn.name, obj.room_jid, parent_win)
|
|
elif gc_control:
|
|
gc_control.print_conversation('Error %s: %s' % (obj.errcode,
|
|
obj.errmsg))
|
|
if gc_control and gc_control.autorejoin:
|
|
gc_control.autorejoin = False
|
|
|
|
def handle_event_presence(self, obj):
|
|
# 'NOTIFY' (account, (jid, status, status message, resource,
|
|
# priority, # keyID, timestamp, contact_nickname))
|
|
#
|
|
# Contact changed show
|
|
account = obj.conn.name
|
|
jid = obj.jid
|
|
|
|
# unset custom status
|
|
if (obj.old_show == 0 and obj.new_show > 1) or \
|
|
(obj.old_show > 1 and obj.new_show == 0 and obj.conn.connected > 1):
|
|
if account in self.status_sent_to_users and \
|
|
jid in self.status_sent_to_users[account]:
|
|
del self.status_sent_to_users[account][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)
|
|
|
|
def handle_event_msgerror(self, obj):
|
|
#'MSGERROR' (account, (jid, error_code, error_msg, msg, time[session]))
|
|
account = obj.conn.name
|
|
jids = obj.fjid.split('/', 1)
|
|
jid = jids[0]
|
|
|
|
session = obj.session
|
|
|
|
gc_control = self.msg_win_mgr.get_gc_control(jid, account)
|
|
if not gc_control and \
|
|
jid in self.minimized_controls[account]:
|
|
gc_control = self.minimized_controls[account][jid]
|
|
if gc_control and gc_control.type_id != message_control.TYPE_GC:
|
|
gc_control = None
|
|
if gc_control:
|
|
if len(jids) > 1: # it's a pm
|
|
nick = jids[1]
|
|
|
|
if session:
|
|
ctrl = session.control
|
|
else:
|
|
ctrl = self.msg_win_mgr.get_control(obj.fjid, account)
|
|
|
|
if not ctrl:
|
|
tv = gc_control.list_treeview
|
|
model = tv.get_model()
|
|
iter_ = gc_control.get_contact_iter(nick)
|
|
if iter_:
|
|
show = model[iter_][3]
|
|
else:
|
|
show = 'offline'
|
|
gc_c = app.contacts.create_gc_contact(room_jid=jid,
|
|
account=account, name=nick, show=show)
|
|
ctrl = self.new_private_chat(gc_c, account, session)
|
|
|
|
ctrl.print_conversation(_('Error %(code)s: %(msg)s') % {
|
|
'code': obj.error_code, 'msg': obj.error_msg}, 'status')
|
|
return
|
|
|
|
gc_control.print_conversation(_('Error %(code)s: %(msg)s') % {
|
|
'code': obj.error_code, 'msg': obj.error_msg}, 'status')
|
|
if gc_control.parent_win and \
|
|
gc_control.parent_win.get_active_jid() == jid:
|
|
gc_control.set_subject(gc_control.subject)
|
|
return
|
|
|
|
if app.jid_is_transport(jid):
|
|
jid = jid.replace('@', '')
|
|
msg = obj.error_msg
|
|
if obj.msg:
|
|
msg = _('error while sending %(message)s ( %(error)s )') % {
|
|
'message': obj.msg, 'error': msg}
|
|
if session:
|
|
session.roster_message(jid, msg, obj.time_, msg_type='error')
|
|
|
|
@staticmethod
|
|
def handle_event_msgsent(obj):
|
|
#('MSGSENT', account, (jid, msg, keyID))
|
|
# do not play sound when standalone chatstate message (eg no msg)
|
|
if obj.message and app.config.get_per('soundevents', 'message_sent',
|
|
'enabled'):
|
|
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:
|
|
if obj.jid in self.instances[account]['sub_request']:
|
|
self.instances[account]['sub_request'][obj.jid].destroy()
|
|
self.instances[account]['sub_request'][obj.jid] = \
|
|
dialogs.SubscriptionRequestWindow(obj.jid, obj.status, account,
|
|
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, obj):
|
|
#('SUBSCRIBED', account, (jid, resource))
|
|
account = obj.conn.name
|
|
if obj.jid in app.contacts.get_jid_list(account):
|
|
c = app.contacts.get_first_contact_from_jid(account, obj.jid)
|
|
c.resource = obj.resource
|
|
self.roster.remove_contact_from_groups(c.jid, account,
|
|
[_('Not in Roster'), _('Observers')], update=False)
|
|
else:
|
|
keyID = ''
|
|
attached_keys = app.config.get_per('accounts', account,
|
|
'attached_gpg_keys').split()
|
|
if obj.jid in attached_keys:
|
|
keyID = attached_keys[attached_keys.index(obj.jid) + 1]
|
|
name = obj.jid.split('@', 1)[0]
|
|
name = name.split('%', 1)[0]
|
|
contact1 = app.contacts.create_contact(jid=obj.jid,
|
|
account=account, name=name, groups=[], show='online',
|
|
status='online', ask='to', resource=obj.resource, keyID=keyID)
|
|
app.contacts.add_contact(account, contact1)
|
|
self.roster.add_contact(obj.jid, account)
|
|
InformationDialog(_('Authorization accepted'),
|
|
_('The contact "%s" has authorized you to see their status.')
|
|
% obj.jid)
|
|
|
|
def show_unsubscribed_dialog(self, account, contact):
|
|
def on_yes(is_checked, list_):
|
|
self.roster.on_req_usub(None, list_)
|
|
list_ = [(contact, account)]
|
|
YesNoDialog(
|
|
_('Contact "%s" removed subscription from you') % contact.jid,
|
|
_('You will always see them as offline.\nDo you want to '
|
|
'remove them from your contact list?'),
|
|
on_response_yes=(on_yes, list_))
|
|
# 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_config(self, obj):
|
|
#('GC_CONFIG', account, (jid, form_node)) config is a dict
|
|
account = obj.conn.name
|
|
if obj.jid in app.automatic_rooms[account]:
|
|
if 'continue_tag' in app.automatic_rooms[account][obj.jid]:
|
|
# We're converting chat to muc. allow participants to invite
|
|
for f in obj.dataform.iter_fields():
|
|
if f.var == 'muc#roomconfig_allowinvites':
|
|
f.value = True
|
|
elif f.var == 'muc#roomconfig_publicroom':
|
|
f.value = False
|
|
elif f.var == 'muc#roomconfig_membersonly':
|
|
f.value = True
|
|
elif f.var == 'public_list':
|
|
f.value = False
|
|
obj.conn.get_module('MUC').set_config(obj.jid, obj.dataform.get_purged())
|
|
user_list = {}
|
|
for jid in app.automatic_rooms[account][obj.jid]['invities']:
|
|
user_list[jid] = {'affiliation': 'member'}
|
|
obj.conn.get_module('MUC').set_affiliation(obj.jid, user_list)
|
|
else:
|
|
# use default configuration
|
|
obj.conn.get_module('MUC').set_config(obj.jid, obj.form_node)
|
|
# invite contacts
|
|
# check if it is necessary to add <continue />
|
|
continue_tag = False
|
|
if 'continue_tag' in app.automatic_rooms[account][obj.jid]:
|
|
continue_tag = True
|
|
if 'invities' in app.automatic_rooms[account][obj.jid]:
|
|
for jid in app.automatic_rooms[account][obj.jid]['invities']:
|
|
obj.conn.get_module('MUC').invite(
|
|
obj.jid, jid, continue_=continue_tag)
|
|
gc_control = self.msg_win_mgr.get_gc_control(obj.jid,
|
|
account)
|
|
if gc_control:
|
|
gc_control.print_conversation(
|
|
_('%(jid)s has been invited in this room') % {
|
|
'jid': jid}, graphics=False)
|
|
del app.automatic_rooms[account][obj.jid]
|
|
elif obj.jid not in self.instances[account]['gc_config']:
|
|
self.instances[account]['gc_config'][obj.jid] = \
|
|
config.GroupchatConfigWindow(account, obj.jid, obj.dataform)
|
|
|
|
def handle_event_gc_affiliation(self, obj):
|
|
#('GC_AFFILIATION', account, (room_jid, users_dict))
|
|
account = obj.conn.name
|
|
if obj.room_jid in self.instances[account]['gc_config']:
|
|
self.instances[account]['gc_config'][obj.room_jid].\
|
|
affiliation_list_received(obj.users_dict)
|
|
|
|
def handle_event_gc_decline(self, obj):
|
|
gc_control = self.msg_win_mgr.get_gc_control(obj.room_jid, obj.account)
|
|
if gc_control:
|
|
if obj.reason:
|
|
gc_control.print_conversation(
|
|
_('%(jid)s declined the invitation: %(reason)s') % {
|
|
'jid': obj.from_, 'reason': obj.reason},
|
|
graphics=False)
|
|
else:
|
|
gc_control.print_conversation(
|
|
_('%(jid)s declined the invitation') % {
|
|
'jid': obj.from_}, graphics=False)
|
|
|
|
def handle_event_gc_invitation(self, obj):
|
|
if helpers.allow_popup_window(obj.account) or not self.systray_enabled:
|
|
dialogs.InvitationReceivedDialog(
|
|
obj.account, obj.room_jid,
|
|
str(obj.from_), obj.password, obj.reason,
|
|
is_continued=obj.is_continued)
|
|
return
|
|
|
|
event = events.GcInvitationtEvent(
|
|
obj.room_jid, obj.reason,
|
|
obj.password, obj.is_continued, str(obj.from_))
|
|
self.add_event(obj.account, str(obj.from_), event)
|
|
|
|
if helpers.allow_showing_notification(obj.account):
|
|
event_type = _('Groupchat Invitation')
|
|
text = _('You are invited to {room} by {user}').format(
|
|
room=obj.room_jid, user=str(obj.from_))
|
|
app.notification.popup(
|
|
event_type, str(obj.from_), obj.account, 'gc-invitation',
|
|
'gajim-gc_invitation', event_type, text, room_jid=obj.room_jid)
|
|
|
|
def forget_gpg_passphrase(self, keyid):
|
|
if keyid in self.gpg_passphrase:
|
|
del self.gpg_passphrase[keyid]
|
|
return False
|
|
|
|
def handle_event_bad_gpg_passphrase(self, obj):
|
|
#('BAD_PASSPHRASE', account, ())
|
|
if obj.use_gpg_agent:
|
|
sectext = _('You configured Gajim to use OpenPGP agent, but there '
|
|
'is no OpenPGP agent running or it returned a wrong passphrase.'
|
|
'\n')
|
|
sectext += _('You are currently connected without your OpenPGP '
|
|
'key.')
|
|
WarningDialog(_('Wrong passphrase'), sectext)
|
|
else:
|
|
account = obj.conn.name
|
|
app.notification.popup(
|
|
'warning', account, account, '', 'dialog-warning',
|
|
_('Wrong OpenPGP passphrase'),
|
|
_('You are currently connected without your OpenPGP key.'))
|
|
self.forget_gpg_passphrase(obj.keyID)
|
|
|
|
@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)
|
|
|
|
dialogs.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_gpg_password_required(self, obj):
|
|
#('GPG_PASSWORD_REQUIRED', account, (callback,))
|
|
if obj.keyid in self.gpg_passphrase:
|
|
request = self.gpg_passphrase[obj.keyid]
|
|
else:
|
|
request = PassphraseRequest(obj.keyid)
|
|
self.gpg_passphrase[obj.keyid] = request
|
|
request.add_callback(obj.conn.name, obj.callback)
|
|
|
|
@staticmethod
|
|
def handle_event_gpg_trust_key(obj):
|
|
#('GPG_ALWAYS_TRUST', account, callback)
|
|
def on_yes(checked):
|
|
if checked:
|
|
obj.conn.gpg.always_trust.append(obj.keyID)
|
|
obj.callback(True)
|
|
|
|
def on_no():
|
|
obj.callback(False)
|
|
|
|
YesNoDialog(_('Untrusted OpenPGP key'), _('The OpenPGP key '
|
|
'used to encrypt this chat is not trusted. Do you really want to '
|
|
'encrypt this message?'), checktext=_('_Do not ask me again'),
|
|
on_response_yes=on_yes, on_response_no=on_no)
|
|
|
|
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):
|
|
if save:
|
|
app.config.set_per('accounts', account, 'savepass', True)
|
|
passwords.save_password(account, passphrase)
|
|
obj.conn.set_password(passphrase)
|
|
del self.pass_dialog[account]
|
|
|
|
def on_cancel():
|
|
self.roster.set_state(account, 'offline')
|
|
self.roster.update_status_combobox()
|
|
del self.pass_dialog[account]
|
|
|
|
self.pass_dialog[account] = dialogs.PassphraseDialog(
|
|
_('Password Required'), text, _('Save password'), ok_handler=on_ok,
|
|
cancel_handler=on_cancel)
|
|
|
|
def handle_oauth2_credentials(self, obj):
|
|
account = obj.conn.name
|
|
def on_ok(refresh):
|
|
app.config.set_per('accounts', account, 'oauth2_refresh_token',
|
|
refresh)
|
|
st = app.config.get_per('accounts', account, 'last_status')
|
|
msg = helpers.from_one_line(app.config.get_per('accounts',
|
|
account, 'last_status_msg'))
|
|
app.interface.roster.send_status(account, st, msg)
|
|
del self.pass_dialog[account]
|
|
|
|
def on_cancel():
|
|
app.config.set_per('accounts', account, 'oauth2_refresh_token',
|
|
'')
|
|
self.roster.set_state(account, 'offline')
|
|
self.roster.update_status_combobox()
|
|
del self.pass_dialog[account]
|
|
|
|
instruction = _('Please copy / paste the refresh token from the website'
|
|
' that has just been opened.')
|
|
self.pass_dialog[account] = InputTextDialog(
|
|
_('Oauth2 Credentials'), instruction, is_modal=False,
|
|
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
|
|
keyID = ''
|
|
attached_keys = app.config.get_per('accounts', account,
|
|
'attached_gpg_keys').split()
|
|
if obj.jid in attached_keys:
|
|
keyID = attached_keys[attached_keys.index(obj.jid) + 1]
|
|
contact = app.contacts.create_contact(jid=obj.jid,
|
|
account=account, name=obj.nickname, groups=obj.groups,
|
|
show='offline', sub=obj.sub, ask=obj.ask, keyID=keyID,
|
|
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_bookmarks(self, obj):
|
|
gui_menu_builder.build_bookmark_menu(obj.account)
|
|
|
|
def handle_event_file_send_error(self, account, array):
|
|
jid = array[0]
|
|
file_props = array[1]
|
|
ft = self.instances['file_transfers']
|
|
ft.set_status(file_props, 'stop')
|
|
|
|
if helpers.allow_popup_window(account):
|
|
ft.show_send_error(file_props)
|
|
return
|
|
|
|
event = events.FileSendErrorEvent(file_props)
|
|
self.add_event(account, jid, event)
|
|
|
|
if helpers.allow_showing_notification(account):
|
|
event_type = _('File Transfer Error')
|
|
app.notification.popup(
|
|
event_type, jid, account,
|
|
'file-send-error', 'gajim-ft_error',
|
|
event_type, 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, 'gajim-ft_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):
|
|
keyID = ''
|
|
attached_keys = app.config.get_per('accounts', account,
|
|
'attached_gpg_keys').split()
|
|
if obj.jid in attached_keys:
|
|
keyID = attached_keys[attached_keys.index(obj.jid) + 1]
|
|
contact = app.contacts.create_not_in_roster_contact(jid=obj.jid,
|
|
account=account, keyID=keyID)
|
|
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='gajim-ft_request', 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_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)
|
|
app.nec.push_incoming_event(FileTransferCompletedEvent(None,
|
|
file_props=file_props))
|
|
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 disn't get the hash, sender probably don'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_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.config.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.config.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 = 'gajim-ft_done'
|
|
elif event_type == _('File Transfer Stopped'):
|
|
txt = _('File transfer of %(filename)s from %(name)s '
|
|
'stopped.') % {'filename': filename, 'name': name}
|
|
icon_name = 'gajim-ft_stopped'
|
|
else: # ft hash error
|
|
txt = _('File transfer of %(filename)s from %(name)s '
|
|
'failed.') % {'filename': filename, 'name': name}
|
|
icon_name = 'gajim-ft_stopped'
|
|
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 = 'gajim-ft_done'
|
|
elif event_type == _('File Transfer Stopped'):
|
|
txt = _('File transfer of %(filename)s to %(name)s '
|
|
'stopped.') % {'filename': filename, 'name': name}
|
|
icon_name = 'gajim-ft_stopped'
|
|
else: # ft hash error
|
|
txt = _('File transfer of %(filename)s to %(name)s '
|
|
'failed.') % {'filename': filename, 'name': name}
|
|
icon_name = 'gajim-ft_stopped'
|
|
else:
|
|
txt = ''
|
|
icon_name = None
|
|
|
|
if app.config.get('notify_on_file_complete') and \
|
|
(app.config.get('autopopupaway') or \
|
|
app.connections[account].connected in (2, 3)):
|
|
# we want to be notified and we are online/chat or we don't mind
|
|
# 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.logger.insert_jid(obj.conn.get_own_jid().getStripped())
|
|
account = obj.conn.name
|
|
app.block_signed_in_notifications[account] = True
|
|
connected = obj.conn.connected
|
|
|
|
pep_supported = obj.conn.get_module('PEP').supported
|
|
|
|
if not idle.Monitor.is_unknown() and connected in (2, 3):
|
|
# we go online or free for chat, so we activate auto status
|
|
app.sleeper_state[account] = 'online'
|
|
elif not ((idle.Monitor.is_away() and connected == 4) or \
|
|
(idle.Monitor.is_xa() and connected == 5)):
|
|
# If we are autoaway/xa and come back after a disconnection, do
|
|
# nothing
|
|
# Else disable autoaway
|
|
app.sleeper_state[account] = 'off'
|
|
|
|
if obj.conn.get_module('MAM').available and app.config.get_per('accounts',
|
|
account, 'sync_logs_with_server'):
|
|
obj.conn.get_module('MAM').request_archive_on_signin()
|
|
|
|
invisible_show = app.SHOW_LIST.index('invisible')
|
|
# We cannot join rooms if we are invisible
|
|
if connected == invisible_show:
|
|
return
|
|
# send currently played music
|
|
if (pep_supported and sys.platform == 'linux' and
|
|
app.config.get_per('accounts', account, 'publish_tune')):
|
|
self.enable_music_listener()
|
|
# enable location listener
|
|
if (pep_supported and app.is_installed('GEOCLUE') and
|
|
app.config.get_per('accounts', account, 'publish_location')):
|
|
location.enable()
|
|
|
|
@staticmethod
|
|
def show_httpupload_progress(file):
|
|
ProgressWindow(file)
|
|
|
|
def send_httpupload(self, chat_control):
|
|
accept_cb = partial(self.on_file_dialog_ok, chat_control)
|
|
FileChooserDialog(accept_cb,
|
|
select_multiple=True,
|
|
transient_for=chat_control.parent_win.window)
|
|
|
|
@staticmethod
|
|
def on_file_dialog_ok(chat_control, paths):
|
|
con = app.connections[chat_control.account]
|
|
groupchat = chat_control.type_id == message_control.TYPE_GC
|
|
for path in paths:
|
|
con.get_module('HTTPUpload').check_file_before_transfer(
|
|
path,
|
|
chat_control.encryption,
|
|
chat_control.contact,
|
|
chat_control.session,
|
|
groupchat)
|
|
|
|
def encrypt_file(self, file, callback):
|
|
app.nec.push_incoming_event(HTTPUploadProgressEvent(
|
|
None, status='encrypt', file=file))
|
|
encryption = file.encryption
|
|
plugin = app.plugin_manager.encryption_plugins[encryption]
|
|
if hasattr(plugin, 'encrypt_file'):
|
|
plugin.encrypt_file(file, None, callback)
|
|
else:
|
|
app.nec.push_incoming_event(HTTPUploadProgressEvent(
|
|
None, status='close', file=file))
|
|
self.raise_dialog('httpupload-encryption-not-available')
|
|
|
|
@staticmethod
|
|
def handle_event_metacontacts(obj):
|
|
app.contacts.define_metacontacts(obj.conn.name, obj.meta_list)
|
|
|
|
@staticmethod
|
|
def handle_atom_entry(obj):
|
|
if obj != PEPEventType.ATOM:
|
|
return
|
|
if obj.get_entry():
|
|
AtomWindow.newAtomEntry(obj.get_entry())
|
|
|
|
def handle_event_zc_name_conflict(self, obj):
|
|
def on_ok(new_name):
|
|
app.config.set_per('accounts', obj.conn.name, 'name', new_name)
|
|
show = obj.conn.old_show
|
|
status = obj.conn.status
|
|
obj.conn.username = new_name
|
|
obj.conn.change_status(show, status)
|
|
def on_cancel():
|
|
obj.conn.change_status('offline', '')
|
|
|
|
InputDialog(_('Username Conflict'),
|
|
_('Please type a new username for your local account'),
|
|
input_str=obj.alt_name, is_modal=True, ok_handler=on_ok,
|
|
cancel_handler=on_cancel, transient_for=self.roster.window)
|
|
|
|
def handle_event_resource_conflict(self, obj):
|
|
# ('RESOURCE_CONFLICT', account, ())
|
|
# First we go offline, but we don't overwrite status message
|
|
account = obj.conn.name
|
|
conn = obj.conn
|
|
self.roster.send_status(account, 'offline', conn.status)
|
|
|
|
def on_ok(new_resource):
|
|
app.config.set_per('accounts', account, 'resource', new_resource)
|
|
self.roster.send_status(account, conn.old_show, conn.status)
|
|
|
|
proposed_resource = conn.server_resource
|
|
if proposed_resource.startswith('gajim.'):
|
|
# Dont notify the user about resource change if he didn't set
|
|
# a custom resource
|
|
on_ok('gajim.$rand')
|
|
return
|
|
|
|
proposed_resource += app.config.get('gc_proposed_nick_char')
|
|
dialogs.ResourceConflictDialog(_('Resource Conflict'),
|
|
_('You are already connected to this account with the same '
|
|
'resource. Please type a new one'), resource=proposed_resource,
|
|
ok_handler=on_ok)
|
|
|
|
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')
|
|
|
|
def handle_event_jingle_incoming(self, obj):
|
|
# ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type,
|
|
# data...))
|
|
# TODO: conditional blocking if peer is not in roster
|
|
|
|
account = obj.conn.name
|
|
content_types = [obj.contents.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
|
|
|
|
ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
|
|
or self.msg_win_mgr.get_control(obj.jid, account))
|
|
if ctrl:
|
|
if 'audio' in content_types:
|
|
ctrl.set_audio_state('connection_received', obj.sid)
|
|
if 'video' in content_types:
|
|
ctrl.set_video_state('connection_received', obj.sid)
|
|
|
|
dlg = dialogs.VoIPCallReceivedDialog.get_dialog(obj.fjid, obj.sid)
|
|
if dlg:
|
|
dlg.add_contents(content_types)
|
|
return
|
|
|
|
if helpers.allow_popup_window(account):
|
|
dialogs.VoIPCallReceivedDialog(account, obj.fjid, obj.sid,
|
|
content_types)
|
|
return
|
|
|
|
event = events.JingleIncomingEvent(obj.fjid, obj.sid, content_types)
|
|
self.add_event(account, obj.jid, event)
|
|
|
|
if helpers.allow_showing_notification(account):
|
|
# TODO: we should use another pixmap ;-)
|
|
txt = _('%s wants to start a voice chat.') % \
|
|
app.get_name_from_jid(account, obj.fjid)
|
|
event_type = _('Voice Chat Request')
|
|
app.notification.popup(
|
|
event_type, obj.fjid, account, 'jingle-incoming',
|
|
icon_name='gajim-mic_active', title=event_type, text=txt)
|
|
|
|
def handle_event_jingle_connected(self, obj):
|
|
# ('JINGLE_CONNECTED', account, (peerjid, sid, media))
|
|
if obj.media in ('audio', 'video'):
|
|
account = obj.conn.name
|
|
ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
|
|
or self.msg_win_mgr.get_control(obj.jid, account))
|
|
if ctrl:
|
|
if obj.media == 'audio':
|
|
ctrl.set_audio_state('connected', obj.sid)
|
|
else:
|
|
ctrl.set_video_state('connected', obj.sid)
|
|
|
|
def handle_event_jingle_disconnected(self, obj):
|
|
# ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason))
|
|
account = obj.conn.name
|
|
ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
|
|
or self.msg_win_mgr.get_control(obj.jid, account))
|
|
if ctrl:
|
|
if obj.media is None:
|
|
ctrl.stop_jingle(sid=obj.sid, reason=obj.reason)
|
|
elif obj.media == 'audio':
|
|
ctrl.set_audio_state('stop', sid=obj.sid, reason=obj.reason)
|
|
elif obj.media == 'video':
|
|
ctrl.set_video_state('stop', sid=obj.sid, reason=obj.reason)
|
|
dialog = dialogs.VoIPCallReceivedDialog.get_dialog(obj.fjid, obj.sid)
|
|
if dialog:
|
|
if obj.media is None:
|
|
dialog.dialog.destroy()
|
|
else:
|
|
dialog.remove_contents((obj.media, ))
|
|
|
|
def handle_event_jingle_error(self, obj):
|
|
# ('JINGLE_ERROR', account, (peerjid, sid, reason))
|
|
account = obj.conn.name
|
|
ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
|
|
or self.msg_win_mgr.get_control(obj.jid, account))
|
|
if ctrl and obj.sid == ctrl.audio_sid:
|
|
ctrl.set_audio_state('error', reason=obj.reason)
|
|
|
|
@staticmethod
|
|
def handle_event_roster_item_exchange(obj):
|
|
# data = (action in [add, delete, modify], exchange_list, jid_from)
|
|
dialogs.RosterItemExchangeWindow(obj.conn.name, obj.action,
|
|
obj.exchange_items_list, obj.fjid)
|
|
|
|
def handle_event_ssl_error(self, obj):
|
|
account = obj.conn.name
|
|
server = app.config.get_per('accounts', account, 'hostname')
|
|
|
|
def on_ok(is_checked):
|
|
del self.instances[account]['online_dialog']['ssl_error']
|
|
if is_checked[0]:
|
|
|
|
pem = OpenSSL.crypto.dump_certificate(
|
|
OpenSSL.crypto.FILETYPE_PEM, obj.cert).decode('utf-8')
|
|
|
|
# Check if cert is already in file
|
|
certs = ''
|
|
my_ca_certs = configpaths.get('MY_CACERTS')
|
|
if os.path.isfile(my_ca_certs):
|
|
with open(my_ca_certs, encoding='utf-8') as f:
|
|
certs = f.read()
|
|
if pem in certs:
|
|
ErrorDialog(_('Certificate Already in File'),
|
|
_('This certificate is already in file %s, so it\'s '
|
|
'not added again.') % my_ca_certs)
|
|
else:
|
|
with open(my_ca_certs, 'a', encoding='utf-8') as f:
|
|
f.write(server + '\n')
|
|
f.write(pem + '\n\n')
|
|
|
|
if is_checked[1]:
|
|
ignore_ssl_errors = app.config.get_per('accounts', account,
|
|
'ignore_ssl_errors').split()
|
|
ignore_ssl_errors.append(str(obj.error_num))
|
|
app.config.set_per('accounts', account, 'ignore_ssl_errors',
|
|
' '.join(ignore_ssl_errors))
|
|
obj.conn.process_ssl_errors()
|
|
|
|
def on_cancel():
|
|
del self.instances[account]['online_dialog']['ssl_error']
|
|
obj.conn.disconnect(on_purpose=True)
|
|
app.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
|
|
show='offline'))
|
|
|
|
text = _('The authenticity of the %s '
|
|
'certificate could be invalid') % server
|
|
|
|
default_text = _('\nUnknown SSL error: %d') % obj.error_num
|
|
ssl_error_text = SSLError.get(obj.error_num, default_text)
|
|
text += _('\nSSL Error: <b>%s</b>') % ssl_error_text
|
|
|
|
fingerprint_sha1 = obj.cert.digest('sha1').decode('utf-8')
|
|
fingerprint_sha256 = obj.cert.digest('sha256').decode('utf-8')
|
|
|
|
pritext = _('Error verifying SSL certificate')
|
|
sectext = _('There was an error verifying the SSL certificate of your '
|
|
'XMPP server: %(error)s\nDo you still want to connect to this '
|
|
'server?') % {'error': text}
|
|
if obj.error_num in (18, 27):
|
|
checktext1 = _('Add this certificate to the list of trusted '
|
|
'certificates.\nSHA-1 fingerprint of the certificate:\n%(sha1)s'
|
|
'\nSHA-256 fingerprint of the certificate:\n%(sha256)s') % \
|
|
{'sha1': fingerprint_sha1, 'sha256': fingerprint_sha256}
|
|
else:
|
|
checktext1 = ''
|
|
checktext2 = _('Ignore this error for this certificate.')
|
|
if 'ssl_error' in self.instances[account]['online_dialog']:
|
|
self.instances[account]['online_dialog']['ssl_error'].destroy()
|
|
self.instances[account]['online_dialog']['ssl_error'] = \
|
|
SSLErrorDialog(obj.conn.name, obj.cert, pritext,
|
|
sectext, checktext1, checktext2, on_response_ok=on_ok,
|
|
on_response_cancel=on_cancel)
|
|
self.instances[account]['online_dialog']['ssl_error'].set_title(
|
|
_('SSL Certificate Verification for %s') % account)
|
|
|
|
def handle_event_non_anonymous_server(self, obj):
|
|
account = obj.conn.name
|
|
server = app.config.get_per('accounts', account, 'hostname')
|
|
ErrorDialog(_('Non Anonymous Server'), sectext='Server "%s"'
|
|
'does not support anonymous connection' % server,
|
|
transient_for=self.roster.window)
|
|
|
|
def handle_event_plain_connection(self, obj):
|
|
# ('PLAIN_CONNECTION', account, (connection))
|
|
def on_ok(is_checked):
|
|
if not is_checked[0]:
|
|
if is_checked[1]:
|
|
app.config.set_per('accounts', obj.conn.name,
|
|
'action_when_plaintext_connection', 'disconnect')
|
|
on_cancel()
|
|
return
|
|
# On cancel call del self.instances, so don't call it another time
|
|
# before
|
|
del self.instances[obj.conn.name]['online_dialog']\
|
|
['plain_connection']
|
|
if is_checked[1]:
|
|
app.config.set_per('accounts', obj.conn.name,
|
|
'action_when_plaintext_connection', 'connect')
|
|
obj.conn.connection_accepted(obj.xmpp_client, 'plain')
|
|
|
|
def on_cancel():
|
|
del self.instances[obj.conn.name]['online_dialog']\
|
|
['plain_connection']
|
|
obj.conn.disconnect(on_purpose=True)
|
|
app.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
|
|
show='offline'))
|
|
|
|
if 'plain_connection' in self.instances[obj.conn.name]['online_dialog']:
|
|
self.instances[obj.conn.name]['online_dialog']['plain_connection'].\
|
|
destroy()
|
|
self.instances[obj.conn.name]['online_dialog']['plain_connection'] = \
|
|
PlainConnectionDialog(obj.conn.name, on_ok, on_cancel)
|
|
|
|
def handle_event_insecure_ssl_connection(self, obj):
|
|
# ('INSECURE_SSL_CONNECTION', account, (connection, connection_type))
|
|
def on_ok(is_checked):
|
|
if not is_checked[0]:
|
|
on_cancel()
|
|
return
|
|
del self.instances[obj.conn.name]['online_dialog']['insecure_ssl']
|
|
if is_checked[1]:
|
|
app.config.set_per('accounts', obj.conn.name,
|
|
'warn_when_insecure_ssl_connection', False)
|
|
if obj.conn.connected == 0:
|
|
# We have been disconnecting (too long time since window is
|
|
# opened)
|
|
# re-connect with auto-accept
|
|
obj.conn.connection_auto_accepted = True
|
|
show, msg = obj.conn.continue_connect_info[:2]
|
|
self.roster.send_status(obj.conn.name, show, msg)
|
|
return
|
|
obj.conn.connection_accepted(obj.xmpp_client, obj.conn_type)
|
|
|
|
def on_cancel():
|
|
del self.instances[obj.conn.name]['online_dialog']['insecure_ssl']
|
|
obj.conn.disconnect(on_purpose=True)
|
|
app.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
|
|
show='offline'))
|
|
|
|
pritext = _('Insecure connection')
|
|
sectext = _('You are about to send your password on an insecure '
|
|
'connection. You should install PyOpenSSL to prevent that. Are you '
|
|
'sure you want to do that?')
|
|
checktext1 = _('Yes, I really want to connect insecurely')
|
|
checktext2 = _('_Do not ask me again')
|
|
if 'insecure_ssl' in self.instances[obj.conn.name]['online_dialog']:
|
|
self.instances[obj.conn.name]['online_dialog']['insecure_ssl'].\
|
|
destroy()
|
|
self.instances[obj.conn.name]['online_dialog']['insecure_ssl'] = \
|
|
ConfirmationDialogDoubleCheck(pritext, sectext, checktext1,
|
|
checktext2, on_response_ok=on_ok, on_response_cancel=on_cancel,
|
|
is_modal=False)
|
|
|
|
def handle_event_insecure_password(self, obj):
|
|
# ('INSECURE_PASSWORD', account, ())
|
|
def on_ok(is_checked):
|
|
if not is_checked[0]:
|
|
on_cancel()
|
|
return
|
|
del self.instances[obj.conn.name]['online_dialog']\
|
|
['insecure_password']
|
|
if is_checked[1]:
|
|
app.config.set_per('accounts', obj.conn.name,
|
|
'warn_when_insecure_password', False)
|
|
if obj.conn.connected == 0:
|
|
# We have been disconnecting (too long time since window is
|
|
# opened)
|
|
# re-connect with auto-accept
|
|
obj.conn.connection_auto_accepted = True
|
|
show, msg = obj.conn.continue_connect_info[:2]
|
|
self.roster.send_status(obj.conn.name, show, msg)
|
|
return
|
|
obj.conn.accept_insecure_password()
|
|
|
|
def on_cancel():
|
|
del self.instances[obj.conn.name]['online_dialog']\
|
|
['insecure_password']
|
|
obj.conn.disconnect(on_purpose=True)
|
|
app.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
|
|
show='offline'))
|
|
|
|
pritext = _('Insecure connection')
|
|
sectext = _('You are about to send your password unencrypted on an '
|
|
'insecure connection. Are you sure you want to do that?')
|
|
checktext1 = _('Yes, I really want to connect insecurely')
|
|
checktext2 = _('_Do not ask me again')
|
|
if 'insecure_password' in self.instances[obj.conn.name]\
|
|
['online_dialog']:
|
|
self.instances[obj.conn.name]['online_dialog']\
|
|
['insecure_password'].destroy()
|
|
self.instances[obj.conn.name]['online_dialog']['insecure_password'] = \
|
|
ConfirmationDialogDoubleCheck(pritext, sectext, checktext1,
|
|
checktext2, on_response_ok=on_ok, on_response_cancel=on_cancel,
|
|
is_modal=False)
|
|
|
|
def create_core_handlers_list(self):
|
|
self.handlers = {
|
|
'DB_ERROR': [self.handle_event_db_error],
|
|
'FILE_SEND_ERROR': [self.handle_event_file_send_error],
|
|
'pep-received': [self.handle_atom_entry],
|
|
'bad-gpg-passphrase': [self.handle_event_bad_gpg_passphrase],
|
|
'bookmarks-received': [self.handle_event_bookmarks],
|
|
'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],
|
|
'gc-invitation-received': [self.handle_event_gc_invitation],
|
|
'gc-decline-received': [self.handle_event_gc_decline],
|
|
'gc-presence-received': [self.handle_event_gc_presence],
|
|
'gpg-password-required': [self.handle_event_gpg_password_required],
|
|
'gpg-trust-key': [self.handle_event_gpg_trust_key],
|
|
'http-auth-received': [self.handle_event_http_auth],
|
|
'information': [self.handle_event_information],
|
|
'insecure-password': [self.handle_event_insecure_password],
|
|
'insecure-ssl-connection': \
|
|
[self.handle_event_insecure_ssl_connection],
|
|
'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],
|
|
'jingleFT-cancelled-received': [self.handle_event_jingleft_cancel],
|
|
'message-error': [self.handle_event_msgerror],
|
|
'message-not-sent': [self.handle_event_msgnotsent],
|
|
'message-sent': [self.handle_event_msgsent],
|
|
'metacontacts-received': [self.handle_event_metacontacts],
|
|
'muc-admin-received': [self.handle_event_gc_affiliation],
|
|
'muc-owner-received': [self.handle_event_gc_config],
|
|
'oauth2-credentials-required': [self.handle_oauth2_credentials],
|
|
'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],
|
|
'ssl-error': [self.handle_event_ssl_error],
|
|
'non-anonymous-server-error': [self.handle_event_non_anonymous_server],
|
|
'stream-conflict-received': [self.handle_event_resource_conflict],
|
|
'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],
|
|
}
|
|
|
|
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 = notify.get_show_in_roster(event_type, account, jid)
|
|
show_in_systray = notify.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:
|
|
self.roster.add_to_not_in_the_roster(account, jid)
|
|
|
|
# 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_):
|
|
w = None
|
|
ctrl = None
|
|
|
|
resource = app.get_resource_from_jid(fjid)
|
|
jid = app.get_jid_without_resource(fjid)
|
|
|
|
if type_ == 'connection-lost':
|
|
app.interface.roster.window.present()
|
|
return
|
|
|
|
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
|
|
|
|
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)
|
|
if gc_contact:
|
|
show = gc_contact.show
|
|
else:
|
|
show = 'offline'
|
|
gc_contact = app.contacts.create_gc_contact(
|
|
room_jid=room_jid, account=account, name=nick,
|
|
show=show)
|
|
|
|
ctrl = self.new_private_chat(gc_contact, account)
|
|
|
|
w = ctrl.parent_win
|
|
elif type_ in ('normal', '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_)
|
|
dialogs.InvitationReceivedDialog(account, event.room_jid, jid,
|
|
event.password, event.reason, event.is_continued)
|
|
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_)
|
|
dialogs.SubscriptionRequestWindow(jid, event.text, account,
|
|
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_)
|
|
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()
|
|
|
|
def join_gc_minimal(self, account, room_jid, password=None,
|
|
transient_for=None):
|
|
if account is not None:
|
|
if app.in_groupchat(account, room_jid):
|
|
# If we already in the groupchat, join_gc_room will bring
|
|
# it to front
|
|
app.interface.join_gc_room(account, room_jid, '', '')
|
|
return
|
|
|
|
con = app.connections[account]
|
|
bookmarks = con.get_module('Bookmarks').bookmarks
|
|
bookmark = bookmarks.get(room_jid, None)
|
|
if bookmark is not None:
|
|
app.interface.join_gc_room(
|
|
account, room_jid, bookmark['nick'], bookmark['password'])
|
|
return
|
|
|
|
try:
|
|
room_jid = helpers.parse_jid(room_jid)
|
|
except helpers.InvalidFormat:
|
|
ErrorDialog('Invalid JID',
|
|
transient_for=app.app.get_active_window())
|
|
return
|
|
|
|
connected_accounts = app.get_connected_accounts()
|
|
if account is not None and account not in connected_accounts:
|
|
connected_accounts = None
|
|
if not connected_accounts:
|
|
ErrorDialog(
|
|
_('You are not connected to the server'),
|
|
_('You can not join a group chat unless you are connected.'),
|
|
transient_for=app.app.get_active_window())
|
|
return
|
|
|
|
def _on_discover_result():
|
|
if not muc_caps_cache.is_cached(room_jid):
|
|
ErrorDialog(_('JID is not a Groupchat'),
|
|
transient_for=app.app.get_active_window())
|
|
return
|
|
JoinGroupchatWindow(account, room_jid, password=password,
|
|
transient_for=transient_for)
|
|
|
|
disco_account = connected_accounts[0] if account is None else account
|
|
app.connections[disco_account].get_module('Discovery').disco_muc(
|
|
room_jid, _on_discover_result)
|
|
|
|
################################################################################
|
|
### Methods dealing with emoticons
|
|
################################################################################
|
|
|
|
@property
|
|
def basic_pattern_re(self):
|
|
if not self._basic_pattern_re:
|
|
self._basic_pattern_re = re.compile(self.basic_pattern,
|
|
re.IGNORECASE)
|
|
return self._basic_pattern_re
|
|
|
|
@property
|
|
def emot_and_basic_re(self):
|
|
if not self._emot_and_basic_re:
|
|
self._emot_and_basic_re = re.compile(
|
|
self.emot_and_basic, re.IGNORECASE)
|
|
return self._emot_and_basic_re
|
|
|
|
@property
|
|
def sth_at_sth_dot_sth_re(self):
|
|
if not self._sth_at_sth_dot_sth_re:
|
|
self._sth_at_sth_dot_sth_re = re.compile(self.sth_at_sth_dot_sth)
|
|
return self._sth_at_sth_dot_sth_re
|
|
|
|
@property
|
|
def invalid_XML_chars_re(self):
|
|
if not self._invalid_XML_chars_re:
|
|
self._invalid_XML_chars_re = re.compile(self.invalid_XML_chars)
|
|
return self._invalid_XML_chars_re
|
|
|
|
def make_regexps(self):
|
|
# regexp meta characters are: . ^ $ * + ? { } [ ] \ | ( )
|
|
# one escapes the metachars with \
|
|
# \S matches anything but ' ' '\t' '\n' '\r' '\f' and '\v'
|
|
# \s matches any whitespace character
|
|
# \w any alphanumeric character
|
|
# \W any non-alphanumeric character
|
|
# \b means word boundary. This is a zero-width assertion that
|
|
# matches only at the beginning or end of a word.
|
|
# ^ matches at the beginning of lines
|
|
#
|
|
# * means 0 or more times
|
|
# + means 1 or more times
|
|
# ? means 0 or 1 time
|
|
# | means or
|
|
# [^*] anything but '*' (inside [] you don't have to escape metachars)
|
|
# [^\s*] anything but whitespaces and '*'
|
|
# (?<!\S) is a one char lookbehind assertion and asks for any leading
|
|
# whitespace
|
|
# and matches beginning of lines so we have correct formatting detection
|
|
# even if the text is just '*foo*'
|
|
# (?!\S) is the same thing but it's a lookahead assertion
|
|
# \S*[^\s\W] --> in the matching string don't match ? or ) etc.. if at
|
|
# the end
|
|
# so http://be) will match http://be and http://be)be) will match
|
|
# http://be)be
|
|
|
|
self._basic_pattern_re = None
|
|
self._emot_and_basic_re = None
|
|
self._sth_at_sth_dot_sth_re = None
|
|
self._invalid_XML_chars_re = None
|
|
|
|
legacy_prefixes = r"((?<=\()(www|ftp)\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$"\
|
|
r"&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+(?=\)))"\
|
|
r"|((www|ftp)\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]"\
|
|
r"|%[A-Fa-f0-9]{2})+"\
|
|
r"\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+)"
|
|
# NOTE: it's ok to catch www.gr such stuff exist!
|
|
|
|
# FIXME: recognize xmpp: and treat it specially
|
|
links = r"((?<=\()[A-Za-z][A-Za-z0-9\+\.\-]*:"\
|
|
r"([\w\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+"\
|
|
r"(?=\)))|(\w[\w\+\.\-]*:([^<>\s]|%[A-Fa-f0-9]{2})+)"
|
|
|
|
# 2nd one: at_least_one_char@at_least_one_char.at_least_one_char
|
|
mail = r'\bmailto:\S*[^\s\W]|' r'\b\S+@\S+\.\S*[^\s\W]'
|
|
|
|
# detects eg. *b* *bold* *bold bold* test *bold* *bold*! (*bold*)
|
|
# doesn't detect (it's a feature :P) * bold* *bold * * bold * test*bold*
|
|
formatting = r'|(?<!\w)' r'\*[^\s*]' r'([^*]*[^\s*])?' r'\*(?!\w)|'\
|
|
r'(?<!\S)' r'/[^\s/]' r'([^/]*[^\s/])?' r'/(?!\S)|'\
|
|
r'(?<!\w)' r'_[^\s_]' r'([^_]*[^\s_])?' r'_(?!\w)'
|
|
|
|
basic_pattern = links + '|' + mail + '|' + legacy_prefixes
|
|
|
|
link_pattern = basic_pattern
|
|
self.link_pattern_re = re.compile(link_pattern, re.I | re.U)
|
|
|
|
if app.config.get('ascii_formatting'):
|
|
basic_pattern += formatting
|
|
self.basic_pattern = basic_pattern
|
|
|
|
# because emoticons match later (in the string) they need to be after
|
|
# basic matches that may occur earlier
|
|
emoticons = emoji_data.get_regex()
|
|
if app.config.get('ascii_emoticons'):
|
|
emoticons += '|%s' % emoji_ascii_data.get_regex()
|
|
|
|
self.emot_and_basic = '%s|%s' % (basic_pattern, emoticons)
|
|
|
|
# at least one character in 3 parts (before @, after @, after .)
|
|
self.sth_at_sth_dot_sth = r'\S+@\S+\.\S*[^\s)?]'
|
|
|
|
# Invalid XML chars
|
|
self.invalid_XML_chars = '[\x00-\x08]|[\x0b-\x0c]|[\x0e-\x1f]|'\
|
|
'[\ud800-\udfff]|[\ufffe-\uffff]'
|
|
|
|
|
|
################################################################################
|
|
### Methods for opening new messages controls
|
|
################################################################################
|
|
|
|
def join_gc_room(self, account, room_jid, nick, password, minimize=False,
|
|
is_continued=False):
|
|
"""
|
|
Join the room immediately
|
|
"""
|
|
|
|
if app.contacts.get_contact(account, room_jid) and \
|
|
not app.contacts.get_contact(account, room_jid).is_groupchat():
|
|
ErrorDialog(
|
|
_('This is not a group chat'),
|
|
_('%(room_jid)s is already in your roster. Please check '
|
|
'if %(room_jid)s is a correct group chat name. If it is, '
|
|
'delete it from your roster and try joining the group chat '
|
|
'again.') % {'room_jid': room_jid})
|
|
return
|
|
|
|
if not nick:
|
|
nick = app.nicks[account]
|
|
|
|
minimized_control = app.interface.minimized_controls[account].get(
|
|
room_jid, None)
|
|
|
|
if (self.msg_win_mgr.has_window(room_jid, account) or \
|
|
minimized_control) and app.gc_connected[account][room_jid]:
|
|
if self.msg_win_mgr.has_window(room_jid, account):
|
|
gc_ctrl = self.msg_win_mgr.get_gc_control(room_jid, account)
|
|
win = gc_ctrl.parent_win
|
|
win.set_active_tab(gc_ctrl)
|
|
else:
|
|
self.roster.on_groupchat_maximized(None, room_jid, account)
|
|
return
|
|
|
|
invisible_show = app.SHOW_LIST.index('invisible')
|
|
if app.connections[account].connected == invisible_show:
|
|
ErrorDialog(
|
|
_('You cannot join a group chat while you are invisible'))
|
|
return
|
|
|
|
if minimized_control is None and not self.msg_win_mgr.has_window(
|
|
room_jid, account):
|
|
# Join new groupchat
|
|
if minimize:
|
|
# GCMIN
|
|
contact = app.contacts.create_contact(jid=room_jid,
|
|
account=account, groups=[_('Groupchats')], sub='none',
|
|
groupchat=True)
|
|
app.contacts.add_contact(account, contact)
|
|
gc_control = GroupchatControl(None, contact, nick, account)
|
|
app.interface.minimized_controls[account][room_jid] = \
|
|
gc_control
|
|
self.roster.add_groupchat(room_jid, account)
|
|
else:
|
|
self.new_room(room_jid, nick, account,
|
|
is_continued=is_continued)
|
|
elif minimized_control is None:
|
|
# We are already in that groupchat
|
|
gc_control = self.msg_win_mgr.get_gc_control(room_jid, account)
|
|
gc_control.nick = nick
|
|
gc_control.parent_win.set_active_tab(gc_control)
|
|
|
|
# Connect
|
|
app.connections[account].join_gc(nick, room_jid, password)
|
|
if password:
|
|
app.gc_passwords[room_jid] = password
|
|
|
|
def new_room(self, room_jid, nick, account, is_continued=False):
|
|
# Get target window, create a control, and associate it with the window
|
|
# GCMIN
|
|
contact = app.contacts.create_contact(jid=room_jid, account=account,
|
|
groups=[_('Groupchats')], sub='none', groupchat=True)
|
|
app.contacts.add_contact(account, contact)
|
|
mw = self.msg_win_mgr.get_window(contact.jid, account)
|
|
if not mw:
|
|
mw = self.msg_win_mgr.create_window(contact, account,
|
|
GroupchatControl.TYPE_ID)
|
|
gc_control = GroupchatControl(mw, contact, nick, account,
|
|
is_continued=is_continued)
|
|
mw.new_tab(gc_control)
|
|
mw.set_active_tab(gc_control)
|
|
|
|
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, message_control.TYPE_PM)
|
|
|
|
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
|
|
type_ = message_control.TYPE_CHAT
|
|
|
|
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, type_,
|
|
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 in ('offline', 'invisible'):
|
|
ctrl = win.get_control(fjid, account)
|
|
if ctrl:
|
|
ctrl.got_disconnected()
|
|
|
|
################################################################################
|
|
### Other Methods
|
|
################################################################################
|
|
|
|
|
|
def enable_music_listener(self):
|
|
listener = music_track.MusicTrackListener.get()
|
|
if not self.music_track_changed_signal:
|
|
self.music_track_changed_signal = listener.connect(
|
|
'music-track-changed', self.music_track_changed)
|
|
listener.start()
|
|
|
|
def disable_music_listener(self):
|
|
listener = music_track.MusicTrackListener.get()
|
|
listener.disconnect(self.music_track_changed_signal)
|
|
self.music_track_changed_signal = None
|
|
listener.stop()
|
|
|
|
@staticmethod
|
|
def music_track_changed(unused_listener, music_track_info, account=None):
|
|
if not account:
|
|
accounts = app.connections.keys()
|
|
else:
|
|
accounts = [account]
|
|
|
|
if music_track_info is None or music_track_info.paused:
|
|
artist = title = source = ''
|
|
else:
|
|
artist = music_track_info.artist
|
|
title = music_track_info.title
|
|
source = music_track_info.album
|
|
for acct in accounts:
|
|
if not app.account_is_connected(acct):
|
|
continue
|
|
if not app.connections[acct].get_module('PEP').supported:
|
|
continue
|
|
if not app.config.get_per('accounts', acct, 'publish_tune'):
|
|
continue
|
|
if app.connections[acct].music_track_info == music_track_info:
|
|
continue
|
|
app.connections[acct].get_module('UserTune').send(
|
|
(artist, title, source, '', ''))
|
|
app.connections[acct].music_track_info = music_track_info
|
|
|
|
def read_sleepy(self):
|
|
"""
|
|
Check idle status and change that status if needed
|
|
"""
|
|
if not idle.Monitor.poll():
|
|
# idle detection is not supported in that OS
|
|
return False # stop looping in vain
|
|
|
|
for account in app.connections:
|
|
if account not in app.sleeper_state or \
|
|
not app.sleeper_state[account]:
|
|
continue
|
|
if idle.Monitor.is_awake():
|
|
if app.sleeper_state[account] in ('autoaway', 'autoxa'):
|
|
# we go online
|
|
self.roster.send_status(account, 'online',
|
|
app.status_before_autoaway[account])
|
|
app.status_before_autoaway[account] = ''
|
|
app.sleeper_state[account] = 'online'
|
|
if app.sleeper_state[account] == 'idle':
|
|
# we go to the previous state
|
|
connected = app.connections[account].connected
|
|
self.roster.send_status(account, app.SHOW_LIST[connected],
|
|
app.status_before_autoaway[account])
|
|
app.status_before_autoaway[account] = ''
|
|
app.sleeper_state[account] = 'off'
|
|
elif idle.Monitor.is_away() and app.config.get('autoaway'):
|
|
if app.sleeper_state[account] == 'online':
|
|
# we save out online status
|
|
app.status_before_autoaway[account] = \
|
|
app.connections[account].status
|
|
# we go away (no auto status) [we pass True to auto param]
|
|
auto_message = app.config.get('autoaway_message')
|
|
if not auto_message:
|
|
auto_message = app.connections[account].status
|
|
else:
|
|
auto_message = auto_message.replace('$S', '%(status)s')
|
|
auto_message = auto_message.replace('$T', '%(time)s')
|
|
auto_message = auto_message % {
|
|
'status': app.status_before_autoaway[account],
|
|
'time': app.config.get('autoawaytime')
|
|
}
|
|
self.roster.send_status(account, 'away', auto_message,
|
|
auto=True)
|
|
app.sleeper_state[account] = 'autoaway'
|
|
elif app.sleeper_state[account] == 'off':
|
|
# we save out online status
|
|
app.status_before_autoaway[account] = \
|
|
app.connections[account].status
|
|
connected = app.connections[account].connected
|
|
self.roster.send_status(account, app.SHOW_LIST[connected],
|
|
app.status_before_autoaway[account], auto=True)
|
|
app.sleeper_state[account] = 'idle'
|
|
elif idle.Monitor.is_xa() and \
|
|
app.sleeper_state[account] in ('online', 'autoaway',
|
|
'autoaway-forced') and app.config.get('autoxa'):
|
|
# we go extended away [we pass True to auto param]
|
|
auto_message = app.config.get('autoxa_message')
|
|
if not auto_message:
|
|
auto_message = app.connections[account].status
|
|
else:
|
|
auto_message = auto_message.replace('$S', '%(status)s')
|
|
auto_message = auto_message.replace('$T', '%(time)s')
|
|
auto_message = auto_message % {
|
|
'status': app.status_before_autoaway[account],
|
|
'time': app.config.get('autoxatime')
|
|
}
|
|
self.roster.send_status(account, 'xa', auto_message, auto=True)
|
|
app.sleeper_state[account] = 'autoxa'
|
|
return True # renew timeout (loop for ever)
|
|
|
|
def autoconnect(self):
|
|
"""
|
|
Auto connect at startup
|
|
"""
|
|
# dict of account that want to connect sorted by status
|
|
shows = {}
|
|
for a in app.connections:
|
|
if app.config.get_per('accounts', a, 'autoconnect'):
|
|
if app.config.get_per('accounts', a, 'restore_last_status'):
|
|
self.roster.send_status(a, app.config.get_per('accounts',
|
|
a, 'last_status'), helpers.from_one_line(
|
|
app.config.get_per('accounts', a, 'last_status_msg')))
|
|
continue
|
|
show = app.config.get_per('accounts', a, 'autoconnect_as')
|
|
if show not in app.SHOW_LIST:
|
|
continue
|
|
if show not in shows:
|
|
shows[show] = [a]
|
|
else:
|
|
shows[show].append(a)
|
|
def on_message(message, pep_dict):
|
|
if message is None:
|
|
return
|
|
for a in shows[show]:
|
|
self.roster.send_status(a, show, message)
|
|
self.roster.send_pep(a, pep_dict)
|
|
for show in shows:
|
|
self.roster.get_status_message(show, on_message)
|
|
return False
|
|
|
|
def show_systray(self):
|
|
self.systray_enabled = True
|
|
self.systray.show_icon()
|
|
|
|
def hide_systray(self):
|
|
self.systray_enabled = False
|
|
self.systray.hide_icon()
|
|
|
|
@staticmethod
|
|
def on_launch_browser_mailer(widget, url, kind):
|
|
helpers.launch_browser_mailer(kind, url)
|
|
|
|
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():
|
|
err_str = parser.write()
|
|
if err_str is not None:
|
|
print(err_str, file=sys.stderr)
|
|
# it is good to notify the user
|
|
# in case he or she cannot see the output of the console
|
|
error_dialog = ErrorDialog(_('Could not save your settings and '
|
|
'preferences'), err_str)
|
|
error_dialog.run()
|
|
sys.exit()
|
|
|
|
@staticmethod
|
|
def update_avatar(account=None, jid=None, contact=None, room_avatar=False):
|
|
if room_avatar:
|
|
app.nec.push_incoming_event(
|
|
UpdateRoomAvatarEvent(None, account=account, jid=jid))
|
|
elif contact is None:
|
|
app.nec.push_incoming_event(
|
|
UpdateRosterAvatarEvent(None, account=account, jid=jid))
|
|
else:
|
|
app.nec.push_incoming_event(
|
|
UpdateGCAvatarEvent(None, contact=contact))
|
|
|
|
def save_avatar(self, data, publish=False):
|
|
"""
|
|
Save an avatar to the harddisk
|
|
|
|
:param data: publish=False data must be bytes
|
|
publish=True data must be a path to a file
|
|
|
|
:param publish: If publish is True, the method scales the file
|
|
to AvatarSize.PUBLISH size before saving
|
|
|
|
returns SHA1 value of the avatar or None on error
|
|
"""
|
|
if data is None:
|
|
return
|
|
|
|
if publish:
|
|
with open(data, 'rb') as file:
|
|
data = file.read()
|
|
pixbuf = gtkgui_helpers.get_pixbuf_from_data(data)
|
|
if pixbuf is None:
|
|
return
|
|
|
|
width = pixbuf.get_width()
|
|
height = pixbuf.get_height()
|
|
if width > AvatarSize.PUBLISH or height > AvatarSize.PUBLISH:
|
|
# Scale only down, never up
|
|
width, height = gtkgui_helpers.scale_with_ratio(
|
|
AvatarSize.PUBLISH, width, height)
|
|
pixbuf = pixbuf.scale_simple(width,
|
|
height,
|
|
GdkPixbuf.InterpType.BILINEAR)
|
|
publish_path = os.path.join(
|
|
configpaths.get('AVATAR'), 'temp_publish')
|
|
pixbuf.savev(publish_path, 'png', [], [])
|
|
with open(publish_path, 'rb') as file:
|
|
data = file.read()
|
|
return self.save_avatar(data)
|
|
|
|
sha = hashlib.sha1(data).hexdigest()
|
|
path = os.path.join(configpaths.get('AVATAR'), sha)
|
|
try:
|
|
with open(path, "wb") as output_file:
|
|
output_file.write(data)
|
|
except Exception:
|
|
app.log('avatar').error('Saving avatar failed', exc_info=True)
|
|
return
|
|
|
|
return sha
|
|
|
|
@staticmethod
|
|
def get_avatar(filename, size=None, scale=None, publish=False):
|
|
if filename is None or '':
|
|
return
|
|
|
|
if size is None and scale is not None:
|
|
raise ValueError
|
|
|
|
if scale is not None:
|
|
size = size * scale
|
|
|
|
if publish:
|
|
path = os.path.join(configpaths.get('AVATAR'), filename)
|
|
with open(path, 'rb') as file:
|
|
data = file.read()
|
|
return data
|
|
|
|
try:
|
|
pixbuf = app.avatar_cache[filename][size]
|
|
if scale is None:
|
|
return pixbuf
|
|
return Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale)
|
|
except KeyError:
|
|
pass
|
|
|
|
path = os.path.join(configpaths.get('AVATAR'), filename)
|
|
if not os.path.isfile(path):
|
|
return
|
|
|
|
pixbuf = None
|
|
try:
|
|
if size is not None:
|
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
|
path, size, size, True)
|
|
else:
|
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
|
|
except GLib.GError:
|
|
app.log('avatar').info(
|
|
'loading avatar %s failed. Try to convert '
|
|
'avatar image using pillow', filename)
|
|
try:
|
|
with open(path, 'rb') as im_handle:
|
|
img = Image.open(im_handle)
|
|
avatar = img.convert("RGBA")
|
|
except (NameError, OSError):
|
|
app.log('avatar').warning('Pillow convert failed: %s', filename)
|
|
app.log('avatar').debug('Error', exc_info=True)
|
|
return
|
|
array = GLib.Bytes.new(avatar.tobytes())
|
|
width, height = avatar.size
|
|
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
|
|
array, GdkPixbuf.Colorspace.RGB, True,
|
|
8, width, height, width * 4)
|
|
if size:
|
|
width, height = gtkgui_helpers.scale_with_ratio(
|
|
size, width, height)
|
|
pixbuf = pixbuf.scale_simple(
|
|
width, height, GdkPixbuf.InterpType.BILINEAR)
|
|
|
|
if filename not in app.avatar_cache:
|
|
app.avatar_cache[filename] = {}
|
|
app.avatar_cache[filename][size] = pixbuf
|
|
|
|
if scale is None:
|
|
return pixbuf
|
|
return Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale)
|
|
|
|
@staticmethod
|
|
def avatar_exists(filename):
|
|
path = os.path.join(configpaths.get('AVATAR'), filename)
|
|
if not os.path.isfile(path):
|
|
return False
|
|
return True
|
|
|
|
# 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.type_id == message_control.TYPE_GC
|
|
|
|
@staticmethod
|
|
def get_pep_icon(pep_obj):
|
|
if pep_obj == PEPEventType.MOOD:
|
|
received_mood = pep_obj.data['mood']
|
|
mood = received_mood if received_mood in MOODS else 'unknown'
|
|
return gtkgui_helpers.load_mood_icon(mood).get_pixbuf()
|
|
|
|
if pep_obj == PEPEventType.TUNE:
|
|
path = os.path.join(
|
|
configpaths.get('DATA'), 'emoticons', 'static', 'music.png')
|
|
return GdkPixbuf.Pixbuf.new_from_file(path)
|
|
|
|
if pep_obj == PEPEventType.ACTIVITY:
|
|
pep_ = pep_obj.data
|
|
activity = pep_['activity']
|
|
|
|
has_known_activity = activity in ACTIVITIES
|
|
has_known_subactivity = (has_known_activity and
|
|
'subactivity' in pep_ and
|
|
pep_['subactivity'] in ACTIVITIES[activity])
|
|
|
|
if has_known_activity:
|
|
if has_known_subactivity:
|
|
subactivity = pep_['subactivity']
|
|
return gtkgui_helpers.load_activity_icon(
|
|
activity, subactivity).get_pixbuf()
|
|
return gtkgui_helpers.load_activity_icon(activity).get_pixbuf()
|
|
return gtkgui_helpers.load_activity_icon('unknown').get_pixbuf()
|
|
|
|
if pep_obj == PEPEventType.LOCATION:
|
|
icon = gtkgui_helpers.get_icon_pixmap(
|
|
'applications-internet', quiet=True)
|
|
return icon
|
|
|
|
@staticmethod
|
|
def create_ipython_window():
|
|
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_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, connected):
|
|
if connected == self.network_state:
|
|
# This callback gets called a lot from GTK with the
|
|
# same state, not only on change.
|
|
return
|
|
self.network_state = connected
|
|
log.debug('NetworkMonitor state change: %s', connected)
|
|
if connected:
|
|
for connection in app.connections.values():
|
|
if connection.connected <= 0 and connection.time_to_reconnect:
|
|
log.info('Connect %s', connection.name)
|
|
connection.reconnect()
|
|
else:
|
|
for connection in app.connections.values():
|
|
if connection.connected > 1:
|
|
log.info('Disconnect %s', connection.name)
|
|
connection.disconnectedReconnCB()
|
|
|
|
def create_zeroconf_default_config(self):
|
|
if app.config.get_per('accounts', app.ZEROCONF_ACC_NAME, 'name'):
|
|
return
|
|
log.info('Creating zeroconf account')
|
|
app.config.add_per('accounts', app.ZEROCONF_ACC_NAME)
|
|
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME,
|
|
'autoconnect', True)
|
|
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME, 'no_log_for',
|
|
'')
|
|
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME, 'password',
|
|
'zeroconf')
|
|
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME,
|
|
'sync_with_global_status', True)
|
|
|
|
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME,
|
|
'custom_port', 5298)
|
|
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME,
|
|
'is_zeroconf', True)
|
|
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME,
|
|
'use_ft_proxies', False)
|
|
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME,
|
|
'active', False)
|
|
|
|
def run(self, application):
|
|
if app.config.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()
|
|
|
|
helpers.update_optional_features()
|
|
# prepopulate data which we are sure of; note: we do not log these info
|
|
for account in app.connections:
|
|
gajimcaps = caps_cache.capscache[
|
|
('sha-1', app.caps_hash[account])]
|
|
gajimcaps.identities = [app.gajim_identity]
|
|
gajimcaps.features = app.gajim_common_features + \
|
|
app.gajim_optional_features[account]
|
|
|
|
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)
|
|
GLib.timeout_add_seconds(app.config.get(
|
|
'check_idle_every_foo_seconds'), self.read_sleepy)
|
|
|
|
def remote_init():
|
|
if app.config.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.jabber_state_images = {'16': {}, '24': {}, '32': {}, 'opened': {},
|
|
'closed': {}}
|
|
self.minimized_controls = {}
|
|
self.status_sent_to_users = {}
|
|
self.status_sent_to_groups = {}
|
|
self.gpg_passphrase = {}
|
|
self.pass_dialog = {}
|
|
self.db_error_dialog = None
|
|
|
|
self.handlers = {}
|
|
self.roster = None
|
|
self._invalid_XML_chars_re = None
|
|
self._basic_pattern_re = None
|
|
self._emot_and_basic_re = None
|
|
self._sth_at_sth_dot_sth_re = None
|
|
self.link_pattern_re = None
|
|
self.invalid_XML_chars = None
|
|
self.basic_pattern = None
|
|
self.emot_and_basic = None
|
|
self.sth_at_sth_dot_sth = None
|
|
|
|
cfg_was_read = parser.read()
|
|
|
|
if not cfg_was_read:
|
|
# enable plugin_installer by default when creating config file
|
|
app.config.set_per('plugins', 'plugin_installer', 'active', True)
|
|
|
|
# Load CSS files
|
|
app.load_css_config()
|
|
|
|
app.logger.reset_shown_unread_messages()
|
|
# override logging settings from config (don't take care of '-q' option)
|
|
if app.config.get('verbose'):
|
|
logging_helpers.set_verbose()
|
|
|
|
for account in app.config.get_per('accounts'):
|
|
if app.config.get_per('accounts', account, 'is_zeroconf'):
|
|
app.ZEROCONF_ACC_NAME = account
|
|
break
|
|
# Is gnome configured to activate row on single click ?
|
|
# try:
|
|
# gi.require_version('GConf', '2.0')
|
|
# from gi.repository import GConf
|
|
# client = GConf.Client.get_default()
|
|
# click_policy = client.get_string(
|
|
# '/apps/nautilus/preferences/click_policy')
|
|
# if click_policy == 'single':
|
|
# app.single_click = True
|
|
# except Exception:
|
|
# pass
|
|
# add default status messages if there is not in the config file
|
|
if not app.config.get_per('statusmsg'):
|
|
default = app.config.statusmsg_default
|
|
for msg in default:
|
|
app.config.add_per('statusmsg', msg)
|
|
app.config.set_per('statusmsg', msg, 'message',
|
|
default[msg][0])
|
|
app.config.set_per('statusmsg', msg, 'activity',
|
|
default[msg][1])
|
|
app.config.set_per('statusmsg', msg, 'subactivity',
|
|
default[msg][2])
|
|
app.config.set_per('statusmsg', msg, 'activity_text',
|
|
default[msg][3])
|
|
app.config.set_per('statusmsg', msg, 'mood',
|
|
default[msg][4])
|
|
app.config.set_per('statusmsg', msg, 'mood_text',
|
|
default[msg][5])
|
|
|
|
# Add Tor proxy if there is not in the config
|
|
if not app.config.get_per('proxies'):
|
|
default = app.config.proxies_default
|
|
for proxy in default:
|
|
app.config.add_per('proxies', proxy)
|
|
app.config.set_per('proxies', proxy, 'type',
|
|
default[proxy][0])
|
|
app.config.set_per('proxies', proxy, 'host',
|
|
default[proxy][1])
|
|
app.config.set_per('proxies', proxy, 'port',
|
|
default[proxy][2])
|
|
|
|
|
|
app.idlequeue = idlequeue.get_idlequeue()
|
|
# resolve and keep current record of resolved hosts
|
|
app.resolver = resolver.get_resolver()
|
|
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 = notify.Notification()
|
|
|
|
self.create_core_handlers_list()
|
|
self.register_core_handlers()
|
|
|
|
self.create_zeroconf_default_config()
|
|
if app.config.get_per('accounts', 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.config.get_per('accounts'):
|
|
if not app.config.get_per('accounts', account, 'is_zeroconf') and\
|
|
app.config.get_per('accounts', account, 'active'):
|
|
app.connections[account] = Connection(account)
|
|
|
|
# gtk hooks
|
|
# Gtk.about_dialog_set_email_hook(self.on_launch_browser_mailer, 'mail')
|
|
# Gtk.about_dialog_set_url_hook(self.on_launch_browser_mailer, 'url')
|
|
# Gtk.link_button_set_uri_hook(self.on_launch_browser_mailer, 'url')
|
|
|
|
self.instances = {}
|
|
|
|
for a in app.connections:
|
|
self.instances[a] = {'infos': {}, 'disco': {}, 'gc_config': {},
|
|
'search': {}, 'online_dialog': {}, 'sub_request': {}}
|
|
# online_dialog contains all dialogs that have a meaning only when
|
|
# we are not disconnected
|
|
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.config.get_per('accounts', a, 'name')
|
|
app.block_signed_in_notifications[a] = True
|
|
app.sleeper_state[a] = 0
|
|
app.last_message_time[a] = {}
|
|
app.status_before_autoaway[a] = ''
|
|
app.gajim_optional_features[a] = []
|
|
app.caps_hash[a] = ''
|
|
|
|
self.remote_ctrl = None
|
|
|
|
# Handle screensaver
|
|
if sys.platform == 'linux':
|
|
logind.enable()
|
|
screensaver.enable()
|
|
|
|
self.show_vcard_when_connect = []
|
|
|
|
idle.Monitor.set_interval(app.config.get('autoawaytime') * 60,
|
|
app.config.get('autoxatime') * 60)
|
|
|
|
gtkgui_helpers.make_jabber_state_images()
|
|
|
|
self.systray_enabled = False
|
|
|
|
from gajim import statusicon
|
|
self.systray = statusicon.StatusIcon()
|
|
|
|
pixs = []
|
|
for size in (16, 32, 48, 64, 128):
|
|
pix = gtkgui_helpers.get_icon_pixmap('org.gajim.Gajim', size)
|
|
if pix:
|
|
pixs.append(pix)
|
|
if pixs:
|
|
# set the icon to all windows
|
|
Gtk.Window.set_default_icon_list(pixs)
|
|
|
|
# Init emoji_chooser
|
|
from gajim.gtk.emoji_chooser import emoji_chooser
|
|
emoji_chooser.load()
|
|
self.make_regexps()
|
|
|
|
# get transports type from DB
|
|
app.transport_type = app.logger.get_transports_type()
|
|
|
|
if app.config.get('soundplayer') == '':
|
|
# only on first time Gajim starts
|
|
commands = ('paplay', 'aplay', 'play', 'ossplay')
|
|
for command in commands:
|
|
if helpers.is_in_path(command):
|
|
if command == 'paplay':
|
|
command += ' -n gajim --property=media.role=event'
|
|
if command in ('aplay', 'play'):
|
|
command += ' -q'
|
|
elif command == 'ossplay':
|
|
command += ' -qq'
|
|
app.config.set('soundplayer', command)
|
|
break
|
|
|
|
self.last_ftwindow_update = 0
|
|
|
|
self.music_track_changed_signal = None
|
|
|
|
self.network_monitor = Gio.NetworkMonitor.get_default()
|
|
self.network_monitor.connect('network-changed',
|
|
self.network_status_changed)
|
|
self.network_state = self.network_monitor.get_network_available()
|
|
|
|
|
|
class PassphraseRequest:
|
|
def __init__(self, keyid):
|
|
self.keyid = keyid
|
|
self.callbacks = []
|
|
self.dialog_created = False
|
|
self.dialog = None
|
|
self.passphrase = None
|
|
self.completed = False
|
|
|
|
def interrupt(self, account=None):
|
|
if account:
|
|
for (acct, cb) in self.callbacks:
|
|
if acct == account:
|
|
self.callbacks.remove((acct, cb))
|
|
else:
|
|
self.callbacks = []
|
|
if not self.callbacks:
|
|
self.dialog.window.destroy()
|
|
|
|
def run_callback(self, account, callback):
|
|
app.connections[account].gpg_passphrase(self.passphrase)
|
|
callback()
|
|
|
|
def add_callback(self, account, cb):
|
|
if self.completed:
|
|
self.run_callback(account, cb)
|
|
else:
|
|
self.callbacks.append((account, cb))
|
|
if not self.dialog_created:
|
|
self.create_dialog(account)
|
|
|
|
def complete(self, passphrase):
|
|
self.passphrase = passphrase
|
|
self.completed = True
|
|
if passphrase is not None:
|
|
GLib.timeout_add_seconds(30, app.interface.forget_gpg_passphrase,
|
|
self.keyid)
|
|
for (account, cb) in self.callbacks:
|
|
self.run_callback(account, cb)
|
|
self.callbacks = []
|
|
|
|
def create_dialog(self, account):
|
|
title = _('Passphrase Required')
|
|
second = _('Enter OpenPGP key passphrase for key %(keyid)s '
|
|
'(account %(account)s).') % {'keyid': self.keyid,
|
|
'account': account}
|
|
|
|
def _cancel():
|
|
# user cancelled, continue without GPG
|
|
self.complete(None)
|
|
|
|
def _ok(passphrase, checked, count):
|
|
result = app.connections[account].test_gpg_passphrase(passphrase)
|
|
if result == 'ok':
|
|
# passphrase is good
|
|
self.complete(passphrase)
|
|
return
|
|
|
|
if result == 'expired':
|
|
ErrorDialog(
|
|
_('OpenPGP key expired'),
|
|
_('Your OpenPGP key has expired, you will be connected to '
|
|
'%s without OpenPGP.') % account)
|
|
# Don't try to connect with GPG
|
|
app.connections[account].continue_connect_info[2] = False
|
|
self.complete(None)
|
|
return
|
|
|
|
if count < 3:
|
|
# ask again
|
|
dialogs.PassphraseDialog(
|
|
_('Wrong Passphrase'),
|
|
_('Please retype your OpenPGP passphrase or press Cancel.'),
|
|
ok_handler=(_ok, count + 1), cancel_handler=_cancel)
|
|
else:
|
|
# user failed 3 times, continue without GPG
|
|
self.complete(None)
|
|
|
|
self.dialog = dialogs.PassphraseDialog(
|
|
title, second, ok_handler=(_ok, 1), cancel_handler=_cancel)
|
|
self.dialog_created = True
|
|
|
|
|
|
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()
|