1# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org> 2# Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org> 3# Copyright (C) 2005 Alex Podaras <bigpod AT gmail.com> 4# Norman Rasmussen <norman AT rasmussen.co.za> 5# Stéphan Kochen <stephan AT kochen.nl> 6# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com> 7# Alex Mauer <hawke AT hawkesnest.net> 8# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com> 9# Nikos Kouremenos <kourem AT gmail.com> 10# Copyright (C) 2006 Junglecow J <junglecow AT gmail.com> 11# Stefan Bethge <stefan AT lanpartei.de> 12# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org> 13# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net> 14# James Newton <redshodan AT gmail.com> 15# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com> 16# Julien Pivotto <roidelapluie AT gmail.com> 17# Stephan Erb <steve-e AT h3c.de> 18# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org> 19# 20# This file is part of Gajim. 21# 22# Gajim is free software; you can redistribute it and/or modify 23# it under the terms of the GNU General Public License as published 24# by the Free Software Foundation; version 3 only. 25# 26# Gajim is distributed in the hope that it will be useful, 27# but WITHOUT ANY WARRANTY; without even the implied warranty of 28# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 29# GNU General Public License for more details. 30# 31# You should have received a copy of the GNU General Public License 32# along with Gajim. If not, see <http://www.gnu.org/licenses/>. 33 34import os 35import sys 36import time 37import json 38import logging 39from functools import partial 40from threading import Thread 41from datetime import datetime 42from importlib.util import find_spec 43from packaging.version import Version as V 44 45from gi.repository import Gtk 46from gi.repository import GLib 47from gi.repository import Gio 48from gi.repository import Soup 49from nbxmpp import idlequeue 50from nbxmpp import Hashes2 51 52from gajim.common import app 53from gajim.common import events 54from gajim.common.dbus import location 55from gajim.common.dbus import logind 56from gajim.common.dbus import music_track 57 58from gajim import gui_menu_builder 59from gajim.dialog_messages import get_dialog 60 61from gajim.chat_control_base import ChatControlBase 62from gajim.chat_control import ChatControl 63from gajim.groupchat_control import GroupchatControl 64from gajim.privatechat_control import PrivateChatControl 65from gajim.message_window import MessageWindowMgr 66 67from gajim.session import ChatControlSession 68 69from gajim.common import idle 70from gajim.common.zeroconf import connection_zeroconf 71from gajim.common import proxy65_manager 72from gajim.common import socks5 73from gajim.common import helpers 74from gajim.common import passwords 75from gajim.common.helpers import ask_for_status_message 76from gajim.common.helpers import get_group_chat_nick 77from gajim.common.structs import MUCData 78from gajim.common.structs import OutgoingMessage 79from gajim.common.nec import NetworkEvent 80from gajim.common.i18n import _ 81from gajim.common.client import Client 82from gajim.common.const import Display 83from gajim.common.const import JingleState 84 85from gajim.common.file_props import FilesProp 86from gajim.common.connection_handlers_events import InformationEvent 87 88from gajim import roster_window 89from gajim.common import ged 90from gajim.common.exceptions import FileError 91 92from gajim.gui.avatar import AvatarStorage 93from gajim.gui.notification import Notification 94from gajim.gui.dialogs import DialogButton 95from gajim.gui.dialogs import ErrorDialog 96from gajim.gui.dialogs import WarningDialog 97from gajim.gui.dialogs import InformationDialog 98from gajim.gui.dialogs import ConfirmationDialog 99from gajim.gui.dialogs import ConfirmationCheckDialog 100from gajim.gui.dialogs import InputDialog 101from gajim.gui.dialogs import PassphraseDialog 102from gajim.gui.filechoosers import FileChooserDialog 103from gajim.gui.filetransfer import FileTransfersWindow 104from gajim.gui.filetransfer_progress import FileTransferProgress 105from gajim.gui.roster_item_exchange import RosterItemExchangeWindow 106from gajim.gui.util import get_show_in_roster 107from gajim.gui.util import get_show_in_systray 108from gajim.gui.util import open_window 109from gajim.gui.util import get_app_window 110from gajim.gui.util import get_app_windows 111from gajim.gui.util import get_color_for_account 112from gajim.gui.const import ControlType 113 114 115log = logging.getLogger('gajim.interface') 116 117class Interface: 118 119################################################################################ 120### Methods handling events from connection 121################################################################################ 122 123 def handle_event_db_error(self, unused, error): 124 #('DB_ERROR', account, error) 125 if self.db_error_dialog: 126 return 127 self.db_error_dialog = ErrorDialog(_('Database Error'), error) 128 def destroyed(win): 129 self.db_error_dialog = None 130 self.db_error_dialog.connect('destroy', destroyed) 131 132 @staticmethod 133 def handle_event_information(obj): 134 if not obj.popup: 135 return 136 137 if obj.dialog_name is not None: 138 get_dialog(obj.dialog_name, *obj.args, **obj.kwargs) 139 return 140 141 if obj.level == 'error': 142 cls = ErrorDialog 143 elif obj.level == 'warn': 144 cls = WarningDialog 145 elif obj.level == 'info': 146 cls = InformationDialog 147 else: 148 return 149 150 cls(obj.pri_txt, GLib.markup_escape_text(obj.sec_txt)) 151 152 @staticmethod 153 def raise_dialog(name, *args, **kwargs): 154 get_dialog(name, *args, **kwargs) 155 156 @staticmethod 157 def handle_event_http_auth(obj): 158 # ('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg)) 159 def _response(account, answer): 160 obj.conn.get_module('HTTPAuth').build_http_auth_answer( 161 obj.stanza, answer) 162 163 account = obj.conn.name 164 message = _('HTTP (%(method)s) Authorization ' 165 'for %(url)s (ID: %(id)s)') % { 166 'method': obj.method, 167 'url': obj.url, 168 'id': obj.iq_id} 169 sec_msg = _('Do you accept this request?') 170 if app.get_number_of_connected_accounts() > 1: 171 sec_msg = _('Do you accept this request (account: %s)?') % account 172 if obj.msg: 173 sec_msg = obj.msg + '\n' + sec_msg 174 message = message + '\n' + sec_msg 175 176 ConfirmationDialog( 177 _('Authorization Request'), 178 _('HTTP Authorization Request'), 179 message, 180 [DialogButton.make('Cancel', 181 text=_('_No'), 182 callback=_response, 183 args=[obj, 'no']), 184 DialogButton.make('Accept', 185 callback=_response, 186 args=[obj, 'yes'])]).show() 187 188 def handle_event_iq_error(self, event): 189 ctrl = self.msg_win_mgr.get_control(event.properties.jid.bare, 190 event.account) 191 if ctrl and ctrl.is_groupchat: 192 ctrl.add_info_message('Error: %s' % event.properties.error) 193 194 @staticmethod 195 def handle_event_connection_lost(obj): 196 # ('CONNECTION_LOST', account, [title, text]) 197 account = obj.conn.name 198 app.notification.popup( 199 _('Connection Failed'), account, account, 200 'connection-lost', 'gajim-connection_lost', obj.title, obj.msg) 201 202 @staticmethod 203 def unblock_signed_in_notifications(account): 204 app.block_signed_in_notifications[account] = False 205 206 def handle_event_status(self, event): 207 if event.show in ('offline', 'error'): 208 # TODO: Close all account windows 209 pass 210 211 if event.show == 'offline': 212 app.block_signed_in_notifications[event.account] = True 213 else: 214 # 30 seconds after we change our status to sth else than offline 215 # we stop blocking notifications of any kind 216 # this prevents from getting the roster items as 'just signed in' 217 # contacts. 30 seconds should be enough time 218 GLib.timeout_add_seconds(30, 219 self.unblock_signed_in_notifications, 220 event.account) 221 222 def handle_event_presence(self, obj): 223 # 'NOTIFY' (account, (jid, status, status message, resource, 224 # priority, timestamp)) 225 # 226 # Contact changed show 227 account = obj.conn.name 228 jid = obj.jid 229 230 if app.jid_is_transport(jid): 231 # It must be an agent 232 233 # transport just signed in/out, don't show 234 # popup notifications for 30s 235 account_jid = account + '/' + jid 236 app.block_signed_in_notifications[account_jid] = True 237 GLib.timeout_add_seconds(30, self.unblock_signed_in_notifications, 238 account_jid) 239 240 ctrl = self.msg_win_mgr.get_control(jid, account) 241 if ctrl and ctrl.session and len(obj.contact_list) > 1: 242 ctrl.remove_session(ctrl.session) 243 244 @staticmethod 245 def handle_event_read_state_sync(event): 246 if event.type.is_groupchat: 247 control = app.get_groupchat_control( 248 event.account, event.jid.bare) 249 if control is None: 250 log.warning('Groupchat control not found') 251 return 252 253 jid = event.jid.bare 254 types = ['printed_gc_msg', 'printed_marked_gc_msg'] 255 256 else: 257 types = ['chat', 'pm', 'printed_chat', 'printed_pm'] 258 jid = event.jid 259 260 control = app.interface.msg_win_mgr.get_control(jid, event.account) 261 262 # Compare with control.last_msg_id. 263 events_ = app.events.get_events(event.account, jid, types) 264 if not events_: 265 log.warning('No Events') 266 return 267 268 if event.type.is_groupchat: 269 id_ = events_[-1].stanza_id or events_[-1].message_id 270 else: 271 id_ = events_[-1].message_id 272 273 if id_ != event.marker_id: 274 return 275 276 if not app.events.remove_events(event.account, jid, types=types): 277 # There were events to remove 278 if control is not None: 279 control.redraw_after_event_removed(event.jid) 280 281 @staticmethod 282 def handle_event_msgsent(obj): 283 if not obj.play_sound: 284 return 285 286 enabled = app.settings.get_soundevent_settings('message_sent')['enabled'] 287 if enabled: 288 if isinstance(obj.jid, list) and len(obj.jid) > 1: 289 return 290 helpers.play_sound('message_sent') 291 292 @staticmethod 293 def handle_event_msgnotsent(obj): 294 #('MSGNOTSENT', account, (jid, ierror_msg, msg, time, session)) 295 msg = _('error while sending %(message)s ( %(error)s )') % { 296 'message': obj.message, 'error': obj.error} 297 if not obj.session: 298 # No session. This can happen when sending a message from 299 # gajim-remote 300 log.warning(msg) 301 return 302 obj.session.roster_message(obj.jid, msg, obj.time_, obj.conn.name, 303 msg_type='error') 304 305 def handle_event_subscribe_presence(self, obj): 306 #('SUBSCRIBE', account, (jid, text, user_nick)) user_nick is JEP-0172 307 account = obj.conn.name 308 if helpers.allow_popup_window(account) or not self.systray_enabled: 309 open_window('SubscriptionRequest', 310 account=account, 311 jid=obj.jid, 312 text=obj.status, 313 user_nick=obj.user_nick) 314 return 315 316 event = events.SubscriptionRequestEvent(obj.status, obj.user_nick) 317 self.add_event(account, obj.jid, event) 318 319 if helpers.allow_showing_notification(account): 320 event_type = _('Subscription request') 321 app.notification.popup( 322 event_type, obj.jid, account, 'subscription_request', 323 'gajim-subscription_request', event_type, obj.jid) 324 325 def handle_event_subscribed_presence(self, event): 326 bare_jid = event.jid.bare 327 resource = event.jid.resource 328 if bare_jid in app.contacts.get_jid_list(event.account): 329 contact = app.contacts.get_first_contact_from_jid(event.account, 330 bare_jid) 331 contact.resource = resource 332 self.roster.remove_contact_from_groups(contact.jid, 333 event.account, 334 [_('Not in contact list'), 335 _('Observers')], 336 update=False) 337 else: 338 name = event.jid.localpart 339 name = name.split('%', 1)[0] 340 contact = app.contacts.create_contact(jid=bare_jid, 341 account=event.account, 342 name=name, 343 groups=[], 344 show='online', 345 status='online', 346 ask='to', 347 resource=resource) 348 app.contacts.add_contact(event.account, contact) 349 self.roster.add_contact(bare_jid, event.account) 350 351 app.notification.popup( 352 None, 353 bare_jid, 354 event.account, 355 title=_('Authorization accepted'), 356 text=_('The contact "%(jid)s" has authorized you' 357 ' to see their status.') % {'jid': event.jid}) 358 359 def show_unsubscribed_dialog(self, account, contact): 360 def _remove(): 361 self.roster.on_req_usub(None, [(contact, account)]) 362 363 name = contact.get_shown_name() 364 jid = contact.jid 365 ConfirmationDialog( 366 _('Subscription Removed'), 367 _('%(name)s (%(jid)s) has removed subscription from you') % { 368 'name': name, 'jid': jid}, 369 _('You will always see this contact as offline.\n' 370 'Do you want to remove them from your contact list?'), 371 [DialogButton.make('Cancel', 372 text=_('_No')), 373 DialogButton.make('Remove', 374 callback=_remove)]).show() 375 376 # FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does 377 # not show deny 378 379 def handle_event_unsubscribed_presence(self, obj): 380 #('UNSUBSCRIBED', account, jid) 381 account = obj.conn.name 382 contact = app.contacts.get_first_contact_from_jid(account, obj.jid) 383 if not contact: 384 return 385 386 if helpers.allow_popup_window(account) or not self.systray_enabled: 387 self.show_unsubscribed_dialog(account, contact) 388 return 389 390 event = events.UnsubscribedEvent(contact) 391 self.add_event(account, obj.jid, event) 392 393 if helpers.allow_showing_notification(account): 394 event_type = _('Unsubscribed') 395 app.notification.popup( 396 event_type, obj.jid, account, 397 'unsubscribed', 'gajim-unsubscribed', 398 event_type, obj.jid) 399 400 def handle_event_gc_decline(self, event): 401 gc_control = self.msg_win_mgr.get_gc_control(str(event.muc), 402 event.account) 403 if gc_control: 404 if event.reason: 405 gc_control.add_info_message( 406 _('%(jid)s declined the invitation: %(reason)s') % { 407 'jid': event.from_, 'reason': event.reason}) 408 else: 409 gc_control.add_info_message( 410 _('%(jid)s declined the invitation') % { 411 'jid': event.from_}) 412 413 def handle_event_gc_invitation(self, event): 414 event = events.GcInvitationtEvent(event) 415 416 if (helpers.allow_popup_window(event.account) or 417 not self.systray_enabled): 418 open_window('GroupChatInvitation', 419 account=event.account, 420 event=event) 421 return 422 423 self.add_event(event.account, str(event.from_), event) 424 425 if helpers.allow_showing_notification(event.account): 426 contact_name = event.get_inviter_name() 427 event_type = _('Group Chat Invitation') 428 text = _('%(contact)s invited you to %(chat)s') % { 429 'contact': contact_name, 'chat': event.info.muc_name} 430 app.notification.popup(event_type, 431 str(event.from_), 432 event.account, 433 'gc-invitation', 434 'gajim-gc_invitation', 435 event_type, 436 text, 437 room_jid=event.muc) 438 439 @staticmethod 440 def handle_event_client_cert_passphrase(obj): 441 def on_ok(passphrase, checked): 442 obj.conn.on_client_cert_passphrase(passphrase, obj.con, obj.port, 443 obj.secure_tuple) 444 445 def on_cancel(): 446 obj.conn.on_client_cert_passphrase('', obj.con, obj.port, 447 obj.secure_tuple) 448 449 PassphraseDialog(_('Certificate Passphrase Required'), 450 _('Enter the certificate passphrase for account %s') % \ 451 obj.conn.name, ok_handler=on_ok, 452 cancel_handler=on_cancel) 453 454 def handle_event_password_required(self, obj): 455 #('PASSWORD_REQUIRED', account, None) 456 account = obj.conn.name 457 if account in self.pass_dialog: 458 return 459 text = _('Enter your password for account %s') % account 460 461 def on_ok(passphrase, save): 462 app.settings.set_account_setting(account, 'savepass', save) 463 passwords.save_password(account, passphrase) 464 obj.on_password(passphrase) 465 del self.pass_dialog[account] 466 467 def on_cancel(): 468 del self.pass_dialog[account] 469 470 self.pass_dialog[account] = PassphraseDialog( 471 _('Password Required'), text, _('Save password'), ok_handler=on_ok, 472 cancel_handler=on_cancel) 473 474 def handle_event_roster_info(self, obj): 475 #('ROSTER_INFO', account, (jid, name, sub, ask, groups)) 476 account = obj.conn.name 477 contacts = app.contacts.get_contacts(account, obj.jid) 478 if (not obj.sub or obj.sub == 'none') and \ 479 (not obj.ask or obj.ask == 'none') and not obj.nickname and \ 480 not obj.groups: 481 # contact removed us. 482 if contacts: 483 self.roster.remove_contact(obj.jid, account, backend=True) 484 return 485 elif not contacts: 486 if obj.sub == 'remove': 487 return 488 # Add new contact to roster 489 490 contact = app.contacts.create_contact(jid=obj.jid, 491 account=account, name=obj.nickname, groups=obj.groups, 492 show='offline', sub=obj.sub, ask=obj.ask, 493 avatar_sha=obj.avatar_sha) 494 app.contacts.add_contact(account, contact) 495 self.roster.add_contact(obj.jid, account) 496 else: 497 # If contact has changed (sub, ask or group) update roster 498 # Mind about observer status changes: 499 # According to xep 0162, a contact is not an observer anymore when 500 # we asked for auth, so also remove him if ask changed 501 old_groups = contacts[0].groups 502 if obj.sub == 'remove': 503 # another of our instance removed a contact. Remove it here too 504 self.roster.remove_contact(obj.jid, account, backend=True) 505 return 506 update = False 507 if contacts[0].sub != obj.sub or contacts[0].ask != obj.ask\ 508 or old_groups != obj.groups: 509 # c.get_shown_groups() has changed. Reflect that in 510 # roster_window 511 self.roster.remove_contact(obj.jid, account, force=True) 512 update = True 513 for contact in contacts: 514 contact.name = obj.nickname or '' 515 contact.sub = obj.sub 516 contact.ask = obj.ask 517 contact.groups = obj.groups or [] 518 if update: 519 self.roster.add_contact(obj.jid, account) 520 # Refilter and update old groups 521 for group in old_groups: 522 self.roster.draw_group(group, account) 523 self.roster.draw_contact(obj.jid, account) 524 if obj.jid in self.instances[account]['sub_request'] and obj.sub in ( 525 'from', 'both'): 526 self.instances[account]['sub_request'][obj.jid].destroy() 527 528 def handle_event_file_send_error(self, event): 529 ft = self.instances['file_transfers'] 530 ft.set_status(event.file_props, 'stop') 531 532 if helpers.allow_popup_window(event.account): 533 ft.show_send_error(event.file_props) 534 return 535 536 event = events.FileSendErrorEvent(event.file_props) 537 self.add_event(event.account, event.jid, event) 538 539 if helpers.allow_showing_notification(event.account): 540 event_type = _('File Transfer Error') 541 app.notification.popup( 542 event_type, event.jid, event.account, 543 'file-send-error', 'dialog-error', 544 event_type, event.file_props.name) 545 546 def handle_event_file_request_error(self, obj): 547 # ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg)) 548 ft = self.instances['file_transfers'] 549 ft.set_status(obj.file_props, 'stop') 550 errno = obj.file_props.error 551 552 if helpers.allow_popup_window(obj.conn.name): 553 if errno in (-4, -5): 554 ft.show_stopped(obj.jid, obj.file_props, obj.error_msg) 555 else: 556 ft.show_request_error(obj.file_props) 557 return 558 559 if errno in (-4, -5): 560 event_class = events.FileErrorEvent 561 msg_type = 'file-error' 562 else: 563 event_class = events.FileRequestErrorEvent 564 msg_type = 'file-request-error' 565 566 event = event_class(obj.file_props) 567 self.add_event(obj.conn.name, obj.jid, event) 568 569 if helpers.allow_showing_notification(obj.conn.name): 570 # Check if we should be notified 571 event_type = _('File Transfer Error') 572 app.notification.popup( 573 event_type, 574 obj.jid, 575 obj.conn.name, 576 msg_type, 577 'dialog-error', 578 title=event_type, 579 text=obj.file_props.name) 580 581 def handle_event_file_request(self, obj): 582 account = obj.conn.name 583 if obj.jid not in app.contacts.get_jid_list(account): 584 contact = app.contacts.create_not_in_roster_contact( 585 jid=obj.jid, account=account) 586 app.contacts.add_contact(account, contact) 587 self.roster.add_contact(obj.jid, account) 588 contact = app.contacts.get_first_contact_from_jid(account, obj.jid) 589 if obj.file_props.session_type == 'jingle': 590 request = \ 591 obj.stanza.getTag('jingle').getTag('content').getTag( 592 'description').getTag('request') 593 if request: 594 # If we get a request instead 595 ft_win = self.instances['file_transfers'] 596 ft_win.add_transfer(account, contact, obj.file_props) 597 return 598 if helpers.allow_popup_window(account): 599 self.instances['file_transfers'].show_file_request( 600 account, contact, obj.file_props) 601 return 602 event = events.FileRequestEvent(obj.file_props) 603 self.add_event(account, obj.jid, event) 604 if helpers.allow_showing_notification(account): 605 txt = _('%s wants to send you a file.') % app.get_name_from_jid( 606 account, obj.jid) 607 event_type = _('File Transfer Request') 608 app.notification.popup( 609 event_type, 610 obj.jid, 611 account, 612 'file-request', 613 icon_name='document-send', 614 title=event_type, 615 text=txt) 616 617 @staticmethod 618 def handle_event_file_error(title, message): 619 ErrorDialog(title, message) 620 621 def handle_event_file_progress(self, account, file_props): 622 if time.time() - self.last_ftwindow_update > 0.5: 623 # Update ft window every 500ms 624 self.last_ftwindow_update = time.time() 625 self.instances['file_transfers'].set_progress( 626 file_props.type_, file_props.sid, file_props.received_len) 627 628 def __compare_hashes(self, account, file_props): 629 session = app.connections[account].get_module( 630 'Jingle').get_jingle_session(jid=None, sid=file_props.sid) 631 ft_win = self.instances['file_transfers'] 632 h = Hashes2() 633 try: 634 file_ = open(file_props.file_name, 'rb') 635 except Exception: 636 return 637 hash_ = h.calculateHash(file_props.algo, file_) 638 file_.close() 639 # If the hash we received and the hash of the file are the same, 640 # then the file is not corrupt 641 jid = file_props.sender 642 if file_props.hash_ == hash_: 643 GLib.idle_add(self.popup_ft_result, account, jid, file_props) 644 GLib.idle_add(ft_win.set_status, file_props, 'ok') 645 else: 646 # Wrong hash, we need to get the file again! 647 file_props.error = -10 648 GLib.idle_add(self.popup_ft_result, account, jid, file_props) 649 GLib.idle_add(ft_win.set_status, file_props, 'hash_error') 650 # End jingle session 651 if session: 652 session.end_session() 653 654 def handle_event_file_rcv_completed(self, account, file_props): 655 ft = self.instances['file_transfers'] 656 if file_props.error == 0: 657 ft.set_progress( 658 file_props.type_, file_props.sid, file_props.received_len) 659 jid = app.get_jid_without_resource(str(file_props.receiver)) 660 app.nec.push_incoming_event( 661 NetworkEvent('file-transfer-completed', 662 file_props=file_props, 663 jid=jid)) 664 665 else: 666 ft.set_status(file_props, 'stop') 667 if not file_props.completed and (file_props.stalled or 668 file_props.paused): 669 return 670 671 if file_props.type_ == 'r': # We receive a file 672 app.socks5queue.remove_receiver(file_props.sid, True, True) 673 if file_props.session_type == 'jingle': 674 if file_props.hash_ and file_props.error == 0: 675 # We compare hashes in a new thread 676 self.hashThread = Thread(target=self.__compare_hashes, 677 args=(account, file_props)) 678 self.hashThread.start() 679 else: 680 # We didn't get the hash, sender probably doesn't 681 # support that 682 jid = file_props.sender 683 self.popup_ft_result(account, jid, file_props) 684 if file_props.error == 0: 685 ft.set_status(file_props, 'ok') 686 session = \ 687 app.connections[account].get_module( 688 'Jingle').get_jingle_session(jid=None, 689 sid=file_props.sid) 690 # End jingle session 691 # TODO: Only if there are no other parallel downloads in 692 # this session 693 if session: 694 session.end_session() 695 else: # We send a file 696 jid = file_props.receiver 697 app.socks5queue.remove_sender(file_props.sid, True, True) 698 self.popup_ft_result(account, jid, file_props) 699 700 def popup_ft_result(self, account, jid, file_props): 701 ft = self.instances['file_transfers'] 702 if helpers.allow_popup_window(account): 703 if file_props.error == 0: 704 if app.settings.get('notify_on_file_complete'): 705 ft.show_completed(jid, file_props) 706 elif file_props.error == -1: 707 ft.show_stopped( 708 jid, 709 file_props, 710 error_msg=_('Remote Contact Stopped Transfer')) 711 elif file_props.error == -6: 712 ft.show_stopped( 713 jid, 714 file_props, 715 error_msg=_('Error Opening File')) 716 elif file_props.error == -10: 717 ft.show_hash_error( 718 jid, 719 file_props, 720 account) 721 elif file_props.error == -12: 722 ft.show_stopped( 723 jid, 724 file_props, 725 error_msg=_('SSL Certificate Error')) 726 return 727 728 msg_type = '' 729 event_type = '' 730 if (file_props.error == 0 and 731 app.settings.get('notify_on_file_complete')): 732 event_class = events.FileCompletedEvent 733 msg_type = 'file-completed' 734 event_type = _('File Transfer Completed') 735 elif file_props.error in (-1, -6): 736 event_class = events.FileStoppedEvent 737 msg_type = 'file-stopped' 738 event_type = _('File Transfer Stopped') 739 elif file_props.error == -10: 740 event_class = events.FileHashErrorEvent 741 msg_type = 'file-hash-error' 742 event_type = _('File Transfer Failed') 743 744 if event_type == '': 745 # FIXME: ugly workaround (this can happen Gajim sent, Gaim recvs) 746 # this should never happen but it does. see process_result() in 747 # socks5.py 748 # who calls this func (sth is really wrong unless this func is also 749 # registered as progress_cb 750 return 751 752 if msg_type: 753 event = event_class(file_props) 754 self.add_event(account, jid, event) 755 756 if file_props is not None: 757 if file_props.type_ == 'r': 758 # Get the name of the sender, as it is in the roster 759 sender = file_props.sender.split('/')[0] 760 name = app.contacts.get_first_contact_from_jid( 761 account, sender).get_shown_name() 762 filename = os.path.basename(file_props.file_name) 763 764 if event_type == _('File Transfer Completed'): 765 txt = _('%(filename)s received from %(name)s.') % { 766 'filename': filename, 767 'name': name} 768 icon_name = 'emblem-default' 769 elif event_type == _('File Transfer Stopped'): 770 txt = _('File transfer of %(filename)s from %(name)s ' 771 'stopped.') % { 772 'filename': filename, 773 'name': name} 774 icon_name = 'process-stop' 775 else: # File transfer hash error 776 txt = _('File transfer of %(filename)s from %(name)s ' 777 'failed.') % { 778 'filename': filename, 779 'name': name} 780 icon_name = 'process-stop' 781 else: 782 receiver = file_props.receiver 783 if hasattr(receiver, 'jid'): 784 receiver = receiver.jid 785 receiver = receiver.split('/')[0] 786 # Get the name of the contact, as it is in the roster 787 name = app.contacts.get_first_contact_from_jid( 788 account, receiver).get_shown_name() 789 filename = os.path.basename(file_props.file_name) 790 if event_type == _('File Transfer Completed'): 791 txt = _('You successfully sent %(filename)s to ' 792 '%(name)s.') % { 793 'filename': filename, 794 'name': name} 795 icon_name = 'emblem-default' 796 elif event_type == _('File Transfer Stopped'): 797 txt = _('File transfer of %(filename)s to %(name)s ' 798 'stopped.') % { 799 'filename': filename, 800 'name': name} 801 icon_name = 'process-stop' 802 else: # File transfer hash error 803 txt = _('File transfer of %(filename)s to %(name)s ' 804 'failed.') % { 805 'filename': filename, 806 'name': name} 807 icon_name = 'process-stop' 808 else: 809 txt = '' 810 icon_name = None 811 812 if (app.settings.get('notify_on_file_complete') and 813 (app.settings.get('autopopupaway') or 814 app.connections[account].status in ('online', 'chat'))): 815 # We want to be notified and we are online/chat or we don't mind 816 # to be bugged when away/na/busy 817 app.notification.popup( 818 event_type, 819 jid, 820 account, 821 msg_type, 822 icon_name=icon_name, 823 title=event_type, 824 text=txt) 825 826 def handle_event_signed_in(self, obj): 827 """ 828 SIGNED_IN event is emitted when we sign in, so handle it 829 """ 830 # ('SIGNED_IN', account, ()) 831 # block signed in notifications for 30 seconds 832 833 # Add our own JID into the DB 834 app.storage.archive.insert_jid(obj.conn.get_own_jid().bare) 835 account = obj.conn.name 836 app.block_signed_in_notifications[account] = True 837 838 pep_supported = obj.conn.get_module('PEP').supported 839 840 if obj.conn.get_module('MAM').available: 841 obj.conn.get_module('MAM').request_archive_on_signin() 842 843 # enable location listener 844 if (pep_supported and app.is_installed('GEOCLUE') and 845 app.settings.get_account_setting(account, 'publish_location')): 846 location.enable() 847 848 if ask_for_status_message(obj.conn.status, signin=True): 849 open_window('StatusChange', status=obj.conn.status) 850 851 def send_httpupload(self, chat_control, path=None): 852 if path is not None: 853 self._send_httpupload(chat_control, path) 854 return 855 856 accept_cb = partial(self.on_file_dialog_ok, chat_control) 857 FileChooserDialog(accept_cb, 858 select_multiple=True, 859 transient_for=chat_control.parent_win.window) 860 861 def on_file_dialog_ok(self, chat_control, paths): 862 for path in paths: 863 self._send_httpupload(chat_control, path) 864 865 def _send_httpupload(self, chat_control, path): 866 con = app.connections[chat_control.account] 867 try: 868 transfer = con.get_module('HTTPUpload').make_transfer( 869 path, 870 chat_control.encryption, 871 chat_control.contact, 872 chat_control.is_groupchat) 873 except FileError as error: 874 app.nec.push_incoming_event(InformationEvent( 875 None, dialog_name='open-file-error2', args=error)) 876 return 877 878 transfer.connect('cancel', self._on_cancel_upload) 879 transfer.connect('state-changed', 880 self._on_http_upload_state_changed) 881 FileTransferProgress(transfer) 882 con.get_module('HTTPUpload').start_transfer(transfer) 883 884 @staticmethod 885 def _on_http_upload_state_changed(transfer, _signal_name, state): 886 if state.is_finished: 887 uri = transfer.get_transformed_uri() 888 889 type_ = 'chat' 890 if transfer.is_groupchat: 891 type_ = 'groupchat' 892 893 message = OutgoingMessage(account=transfer.account, 894 contact=transfer.contact, 895 message=uri, 896 type_=type_, 897 oob_url=uri) 898 899 client = app.get_client(transfer.account) 900 client.send_message(message) 901 902 @staticmethod 903 def _on_cancel_upload(transfer, _signal_name): 904 client = app.get_client(transfer.account) 905 client.get_module('HTTPUpload').cancel_transfer(transfer) 906 907 @staticmethod 908 def handle_event_metacontacts(obj): 909 app.contacts.define_metacontacts(obj.conn.name, obj.meta_list) 910 911 def handle_event_zc_name_conflict(self, obj): 912 def _on_ok(new_name): 913 app.settings.set_account_setting(obj.conn.name, 'name', new_name) 914 obj.conn.username = new_name 915 obj.conn.change_status(obj.conn.status, obj.conn.status_message) 916 917 def _on_cancel(*args): 918 obj.conn.change_status('offline', '') 919 920 InputDialog( 921 _('Username Conflict'), 922 _('Username Conflict'), 923 _('Please enter a new username for your local account'), 924 [DialogButton.make('Cancel', 925 callback=_on_cancel), 926 DialogButton.make('Accept', 927 text=_('_OK'), 928 callback=_on_ok)], 929 input_str=obj.alt_name, 930 transient_for=self.roster.window).show() 931 932 def handle_event_jingleft_cancel(self, obj): 933 ft = self.instances['file_transfers'] 934 file_props = None 935 # get the file_props of our session 936 file_props = FilesProp.getFileProp(obj.conn.name, obj.sid) 937 if not file_props: 938 return 939 ft.set_status(file_props, 'stop') 940 file_props.error = -4 # is it the right error code? 941 ft.show_stopped(obj.jid, file_props, 'Peer cancelled ' + 942 'the transfer') 943 944 # Jingle AV handling 945 def handle_event_jingle_incoming(self, event): 946 # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type, 947 # data...)) 948 # TODO: conditional blocking if peer is not in roster 949 950 account = event.conn.name 951 content_types = [] 952 for item in event.contents: 953 content_types.append(item.media) 954 # check type of jingle session 955 if 'audio' in content_types or 'video' in content_types: 956 # a voip session... 957 # we now handle only voip, so the only thing we will do here is 958 # not to return from function 959 pass 960 else: 961 # unknown session type... it should be declined in common/jingle.py 962 return 963 964 notification_event = events.JingleIncomingEvent( 965 event.fjid, event.sid, content_types) 966 967 ctrl = (self.msg_win_mgr.get_control(event.fjid, account) 968 or self.msg_win_mgr.get_control(event.jid, account)) 969 if ctrl: 970 if 'audio' in content_types: 971 ctrl.set_jingle_state( 972 'audio', 973 JingleState.CONNECTION_RECEIVED, 974 event.sid) 975 if 'video' in content_types: 976 ctrl.set_jingle_state( 977 'video', 978 JingleState.CONNECTION_RECEIVED, 979 event.sid) 980 ctrl.add_call_received_message(notification_event) 981 982 if helpers.allow_popup_window(account): 983 app.interface.new_chat_from_jid(account, event.fjid) 984 ctrl.add_call_received_message(notification_event) 985 return 986 987 self.add_event(account, event.fjid, notification_event) 988 989 if helpers.allow_showing_notification(account): 990 heading = _('Incoming Call') 991 contact = app.get_name_from_jid(account, event.jid) 992 text = _('%s is calling') % contact 993 app.notification.popup( 994 heading, 995 event.fjid, 996 account, 997 'jingle-incoming', 998 icon_name='call-start-symbolic', 999 title=heading, 1000 text=text) 1001 1002 def handle_event_jingle_connected(self, event): 1003 # ('JINGLE_CONNECTED', account, (peerjid, sid, media)) 1004 if event.media in ('audio', 'video'): 1005 account = event.conn.name 1006 ctrl = (self.msg_win_mgr.get_control(event.fjid, account) 1007 or self.msg_win_mgr.get_control(event.jid, account)) 1008 if ctrl: 1009 con = app.connections[account] 1010 session = con.get_module('Jingle').get_jingle_session( 1011 event.fjid, event.sid) 1012 1013 if event.media == 'audio': 1014 content = session.get_content('audio') 1015 ctrl.set_jingle_state( 1016 'audio', 1017 JingleState.CONNECTED, 1018 event.sid) 1019 if event.media == 'video': 1020 content = session.get_content('video') 1021 ctrl.set_jingle_state( 1022 'video', 1023 JingleState.CONNECTED, 1024 event.sid) 1025 1026 # Now, accept the content/sessions. 1027 # This should be done after the chat control is running 1028 if not session.accepted: 1029 session.approve_session() 1030 for content in event.media: 1031 session.approve_content(content) 1032 1033 def handle_event_jingle_disconnected(self, event): 1034 # ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason)) 1035 account = event.conn.name 1036 ctrl = (self.msg_win_mgr.get_control(event.fjid, account) 1037 or self.msg_win_mgr.get_control(event.jid, account)) 1038 if ctrl: 1039 if event.media is None: 1040 ctrl.stop_jingle(sid=event.sid, reason=event.reason) 1041 if event.media == 'audio': 1042 ctrl.set_jingle_state( 1043 'audio', 1044 JingleState.NULL, 1045 sid=event.sid, 1046 reason=event.reason) 1047 if event.media == 'video': 1048 ctrl.set_jingle_state( 1049 'video', 1050 JingleState.NULL, 1051 sid=event.sid, 1052 reason=event.reason) 1053 1054 def handle_event_jingle_error(self, event): 1055 # ('JINGLE_ERROR', account, (peerjid, sid, reason)) 1056 account = event.conn.name 1057 ctrl = (self.msg_win_mgr.get_control(event.fjid, account) 1058 or self.msg_win_mgr.get_control(event.jid, account)) 1059 if ctrl and event.sid == ctrl.jingle['audio'].sid: 1060 ctrl.set_jingle_state( 1061 'audio', 1062 JingleState.ERROR, 1063 reason=event.reason) 1064 1065 @staticmethod 1066 def handle_event_roster_item_exchange(obj): 1067 # data = (action in [add, delete, modify], exchange_list, jid_from) 1068 RosterItemExchangeWindow(obj.conn.name, obj.action, 1069 obj.exchange_items_list, obj.fjid) 1070 1071 def handle_event_plain_connection(self, event): 1072 ConfirmationDialog( 1073 _('Insecure Connection'), 1074 _('Insecure Connection'), 1075 _('You are about to connect to the account %(account)s ' 1076 '(%(server)s) using an insecure connection method. This means ' 1077 'conversations will not be encrypted. Connecting PLAIN is ' 1078 'strongly discouraged.') % { 1079 'account': event.account, 1080 'server': app.get_hostname_from_account(event.account)}, 1081 [DialogButton.make('Cancel', 1082 text=_('_Abort'), 1083 callback=event.abort), 1084 DialogButton.make('Remove', 1085 text=_('_Connect Anyway'), 1086 callback=event.connect)]).show() 1087 1088 def create_core_handlers_list(self): 1089 self.handlers = { 1090 'DB_ERROR': [self.handle_event_db_error], 1091 'file-send-error': [self.handle_event_file_send_error], 1092 'client-cert-passphrase': [ 1093 self.handle_event_client_cert_passphrase], 1094 'connection-lost': [self.handle_event_connection_lost], 1095 'file-request-error': [self.handle_event_file_request_error], 1096 'file-request-received': [self.handle_event_file_request], 1097 'muc-invitation': [self.handle_event_gc_invitation], 1098 'muc-decline': [self.handle_event_gc_decline], 1099 'http-auth-received': [self.handle_event_http_auth], 1100 'information': [self.handle_event_information], 1101 'iq-error-received': [self.handle_event_iq_error], 1102 'jingle-connected-received': [self.handle_event_jingle_connected], 1103 'jingle-disconnected-received': [ 1104 self.handle_event_jingle_disconnected], 1105 'jingle-error-received': [self.handle_event_jingle_error], 1106 'jingle-request-received': [self.handle_event_jingle_incoming], 1107 'jingle-ft-cancelled-received': [self.handle_event_jingleft_cancel], 1108 'message-not-sent': [self.handle_event_msgnotsent], 1109 'message-sent': [self.handle_event_msgsent], 1110 'metacontacts-received': [self.handle_event_metacontacts], 1111 'our-show': [self.handle_event_status], 1112 'password-required': [self.handle_event_password_required], 1113 'plain-connection': [self.handle_event_plain_connection], 1114 'presence-received': [self.handle_event_presence], 1115 'roster-info': [self.handle_event_roster_info], 1116 'roster-item-exchange-received': \ 1117 [self.handle_event_roster_item_exchange], 1118 'signed-in': [self.handle_event_signed_in], 1119 'subscribe-presence-received': [ 1120 self.handle_event_subscribe_presence], 1121 'subscribed-presence-received': [ 1122 self.handle_event_subscribed_presence], 1123 'unsubscribed-presence-received': [ 1124 self.handle_event_unsubscribed_presence], 1125 'zeroconf-name-conflict': [self.handle_event_zc_name_conflict], 1126 'read-state-sync': [self.handle_event_read_state_sync], 1127 } 1128 1129 def register_core_handlers(self): 1130 """ 1131 Register core handlers in Global Events Dispatcher (GED). 1132 1133 This is part of rewriting whole events handling system to use GED. 1134 """ 1135 for event_name, event_handlers in self.handlers.items(): 1136 for event_handler in event_handlers: 1137 prio = ged.GUI1 1138 if isinstance(event_handler, tuple): 1139 prio = event_handler[1] 1140 event_handler = event_handler[0] 1141 app.ged.register_event_handler(event_name, prio, 1142 event_handler) 1143 1144################################################################################ 1145### Methods dealing with app.events 1146################################################################################ 1147 1148 def add_event(self, account, jid, event): 1149 """ 1150 Add an event to the app.events var 1151 """ 1152 # We add it to the app.events queue 1153 # Do we have a queue? 1154 jid = app.get_jid_without_resource(jid) 1155 no_queue = len(app.events.get_events(account, jid)) == 0 1156 # event can be in common.events.* 1157 # event_type can be in advancedNotificationWindow.events_list 1158 event_types = {'file-request': 'ft_request', 1159 'file-completed': 'ft_finished'} 1160 event_type = event_types.get(event.type_) 1161 show_in_roster = get_show_in_roster(event_type, jid) 1162 show_in_systray = get_show_in_systray(event_type, account, jid) 1163 event.show_in_roster = show_in_roster 1164 event.show_in_systray = show_in_systray 1165 app.events.add_event(account, jid, event) 1166 1167 self.roster.show_title() 1168 if no_queue: # We didn't have a queue: we change icons 1169 if app.contacts.get_contact_with_highest_priority(account, jid): 1170 self.roster.draw_contact(jid, account) 1171 else: 1172 groupchat = event.type_ == 'gc-invitation' 1173 self.roster.add_to_not_in_the_roster( 1174 account, jid, groupchat=groupchat) 1175 1176 # Select the big brother contact in roster, it's visible because it has 1177 # events. 1178 family = app.contacts.get_metacontacts_family(account, jid) 1179 if family: 1180 _nearby_family, bb_jid, bb_account = \ 1181 app.contacts.get_nearby_family_and_big_brother(family, 1182 account) 1183 else: 1184 bb_jid, bb_account = jid, account 1185 self.roster.select_contact(bb_jid, bb_account) 1186 1187 def handle_event(self, account, fjid, type_): 1188 if type_ in ('connection-lost', 'connection-failed'): 1189 app.interface.roster.window.present() 1190 return 1191 1192 w = None 1193 ctrl = None 1194 1195 resource = app.get_resource_from_jid(fjid) 1196 jid = app.get_jid_without_resource(fjid) 1197 1198 if type_ in ('printed_gc_msg', 'printed_marked_gc_msg', 'gc_msg'): 1199 w = self.msg_win_mgr.get_window(jid, account) 1200 if jid in self.minimized_controls[account]: 1201 self.roster.on_groupchat_maximized(None, jid, account) 1202 return 1203 ctrl = self.msg_win_mgr.get_gc_control(jid, account) 1204 1205 elif type_ in ('printed_chat', 'chat', ''): 1206 # '' is for log in/out notifications 1207 1208 ctrl = self.msg_win_mgr.search_control(jid, account, resource) 1209 1210 if not ctrl: 1211 highest_contact = app.contacts.\ 1212 get_contact_with_highest_priority(account, jid) 1213 # jid can have a window if this resource was lower when he sent 1214 # message and is now higher because the other one is offline 1215 if resource and highest_contact.resource == resource and \ 1216 not self.msg_win_mgr.has_window(jid, account): 1217 # remove resource of events too 1218 app.events.change_jid(account, fjid, jid) 1219 resource = None 1220 fjid = jid 1221 1222 contact = None 1223 if resource: 1224 contact = app.contacts.get_contact(account, jid, resource) 1225 if not contact: 1226 contact = highest_contact 1227 if not contact: 1228 # Maybe we deleted the contact from the roster 1229 return 1230 1231 ctrl = self.new_chat(contact, account, resource=resource) 1232 1233 app.last_message_time[account][jid] = 0 # long time ago 1234 1235 w = ctrl.parent_win 1236 elif type_ in ('printed_pm', 'pm'): 1237 1238 ctrl = self.msg_win_mgr.get_control(fjid, account) 1239 1240 if not ctrl: 1241 room_jid = jid 1242 nick = resource 1243 gc_contact = app.contacts.get_gc_contact( 1244 account, room_jid, nick) 1245 ctrl = self.new_private_chat(gc_contact, account) 1246 1247 w = ctrl.parent_win 1248 elif type_ in ('file-request', 'file-request-error', 1249 'file-send-error', 'file-error', 'file-stopped', 'file-completed', 1250 'file-hash-error', 'jingle-incoming'): 1251 # Get the first single message event 1252 event = app.events.get_first_event(account, fjid, type_) 1253 if not event: 1254 # default to jid without resource 1255 event = app.events.get_first_event(account, jid, type_) 1256 if not event: 1257 return 1258 # Open the window 1259 self.roster.open_event(account, jid, event) 1260 else: 1261 # Open the window 1262 self.roster.open_event(account, fjid, event) 1263 elif type_ == 'gc-invitation': 1264 event = app.events.get_first_event(account, jid, type_) 1265 if event is None: 1266 return 1267 open_window('GroupChatInvitation', 1268 account=account, 1269 event=event) 1270 app.events.remove_events(account, jid, event) 1271 self.roster.draw_contact(jid, account) 1272 elif type_ == 'subscription_request': 1273 event = app.events.get_first_event(account, jid, type_) 1274 if event is None: 1275 return 1276 open_window('SubscriptionRequest', 1277 account=account, 1278 jid=jid, 1279 text=event.text, 1280 user_nick=event.nick) 1281 app.events.remove_events(account, jid, event) 1282 self.roster.draw_contact(jid, account) 1283 elif type_ == 'unsubscribed': 1284 event = app.events.get_first_event(account, jid, type_) 1285 if event is None: 1286 return 1287 self.show_unsubscribed_dialog(account, event.contact) 1288 app.events.remove_events(account, jid, event) 1289 self.roster.draw_contact(jid, account) 1290 if w: 1291 w.set_active_tab(ctrl) 1292 w.window.present() 1293 # Using isinstance here because we want to catch all derived types 1294 if isinstance(ctrl, ChatControlBase): 1295 ctrl.scroll_to_end() 1296 1297################################################################################ 1298### Methods for opening new messages controls 1299################################################################################ 1300 1301 def show_groupchat(self, account, room_jid): 1302 minimized_control = self.minimized_controls[account].get(room_jid) 1303 if minimized_control is not None: 1304 self.roster.on_groupchat_maximized(None, room_jid, account) 1305 return True 1306 1307 if self.msg_win_mgr.has_window(room_jid, account): 1308 gc_ctrl = self.msg_win_mgr.get_gc_control(room_jid, account) 1309 # FIXME: Access message window directly 1310 gc_ctrl.parent_win.set_active_tab(gc_ctrl) 1311 return True 1312 return False 1313 1314 def create_groupchat_control(self, account, room_jid, muc_data, 1315 minimize=False): 1316 avatar_sha = app.storage.cache.get_muc_avatar_sha(room_jid) 1317 contact = app.contacts.create_contact(jid=room_jid, 1318 account=account, 1319 groups=[_('Group chats')], 1320 sub='none', 1321 avatar_sha=avatar_sha, 1322 groupchat=True) 1323 app.contacts.add_contact(account, contact) 1324 1325 if minimize: 1326 control = GroupchatControl(None, contact, muc_data, account) 1327 app.interface.minimized_controls[account][room_jid] = control 1328 self.roster.add_groupchat(room_jid, account) 1329 1330 else: 1331 mw = self.msg_win_mgr.get_window(room_jid, account) 1332 if not mw: 1333 mw = self.msg_win_mgr.create_window(contact, 1334 account, 1335 ControlType.GROUPCHAT) 1336 control = GroupchatControl(mw, contact, muc_data, account) 1337 mw.new_tab(control) 1338 mw.set_active_tab(control) 1339 1340 @staticmethod 1341 def _create_muc_data(account, room_jid, nick, password, config): 1342 if not nick: 1343 nick = get_group_chat_nick(account, room_jid) 1344 1345 # Fetch data from bookmarks 1346 client = app.get_client(account) 1347 bookmark = client.get_module('Bookmarks').get_bookmark(room_jid) 1348 if bookmark is not None: 1349 if bookmark.password is not None: 1350 password = bookmark.password 1351 1352 return MUCData(room_jid, nick, password, config) 1353 1354 def create_groupchat(self, account, room_jid, config=None): 1355 muc_data = self._create_muc_data(account, room_jid, None, None, config) 1356 self.create_groupchat_control(account, room_jid, muc_data) 1357 app.connections[account].get_module('MUC').create(muc_data) 1358 1359 def show_or_join_groupchat(self, account, room_jid, **kwargs): 1360 if self.show_groupchat(account, room_jid): 1361 return 1362 self.join_groupchat(account, room_jid, **kwargs) 1363 1364 def join_groupchat(self, 1365 account, 1366 room_jid, 1367 password=None, 1368 nick=None, 1369 minimized=False): 1370 1371 if not app.account_is_available(account): 1372 return 1373 1374 muc_data = self._create_muc_data(account, 1375 room_jid, 1376 nick, 1377 password, 1378 None) 1379 self.create_groupchat_control( 1380 account, room_jid, muc_data, minimize=minimized) 1381 1382 app.connections[account].get_module('MUC').join(muc_data) 1383 1384 def new_private_chat(self, gc_contact, account, session=None): 1385 conn = app.connections[account] 1386 if not session and gc_contact.get_full_jid() in conn.sessions: 1387 sessions = [s for s in conn.sessions[gc_contact.get_full_jid()].\ 1388 values() if isinstance(s, ChatControlSession)] 1389 1390 # look for an existing session with a chat control 1391 for s in sessions: 1392 if s.control: 1393 session = s 1394 break 1395 if not session and sessions: 1396 # there are no sessions with chat controls, just take the first 1397 # one 1398 session = sessions[0] 1399 if not session: 1400 # couldn't find an existing ChatControlSession, just make a new one 1401 session = conn.make_new_session(gc_contact.get_full_jid(), None, 1402 'pm') 1403 1404 contact = gc_contact.as_contact() 1405 if not session.control: 1406 message_window = self.msg_win_mgr.get_window( 1407 gc_contact.get_full_jid(), account) 1408 if not message_window: 1409 message_window = self.msg_win_mgr.create_window( 1410 contact, account, ControlType.PRIVATECHAT) 1411 1412 session.control = PrivateChatControl(message_window, gc_contact, 1413 contact, account, session) 1414 message_window.new_tab(session.control) 1415 1416 if app.events.get_events(account, gc_contact.get_full_jid()): 1417 # We call this here to avoid race conditions with widget validation 1418 session.control.read_queue() 1419 1420 return session.control 1421 1422 def new_chat(self, contact, account, resource=None, session=None): 1423 # Get target window, create a control, and associate it with the window 1424 fjid = contact.jid 1425 if resource: 1426 fjid += '/' + resource 1427 1428 mw = self.msg_win_mgr.get_window(fjid, account) 1429 if not mw: 1430 mw = self.msg_win_mgr.create_window( 1431 contact, account, ControlType.CHAT, resource) 1432 1433 chat_control = ChatControl(mw, contact, account, session, resource) 1434 1435 mw.new_tab(chat_control) 1436 1437 if app.events.get_events(account, fjid): 1438 # We call this here to avoid race conditions with widget validation 1439 chat_control.read_queue() 1440 1441 return chat_control 1442 1443 def new_chat_from_jid(self, account, fjid, message=None): 1444 jid, resource = app.get_room_and_nick_from_fjid(fjid) 1445 contact = app.contacts.get_contact(account, jid, resource) 1446 added_to_roster = False 1447 if not contact: 1448 added_to_roster = True 1449 contact = self.roster.add_to_not_in_the_roster(account, jid, 1450 resource=resource) 1451 1452 ctrl = self.msg_win_mgr.get_control(fjid, account) 1453 1454 if not ctrl: 1455 ctrl = self.new_chat(contact, account, 1456 resource=resource) 1457 if app.events.get_events(account, fjid): 1458 ctrl.read_queue() 1459 1460 if message: 1461 buffer_ = ctrl.msg_textview.get_buffer() 1462 buffer_.set_text(message) 1463 mw = ctrl.parent_win 1464 mw.set_active_tab(ctrl) 1465 # For JEP-0172 1466 if added_to_roster: 1467 ctrl.user_nick = app.nicks[account] 1468 1469 return ctrl 1470 1471 def on_open_chat_window(self, widget, contact, account, resource=None, 1472 session=None): 1473 # Get the window containing the chat 1474 fjid = contact.jid 1475 1476 if resource: 1477 fjid += '/' + resource 1478 1479 ctrl = None 1480 1481 if session: 1482 ctrl = session.control 1483 if not ctrl: 1484 win = self.msg_win_mgr.get_window(fjid, account) 1485 1486 if win: 1487 ctrl = win.get_control(fjid, account) 1488 1489 if not ctrl: 1490 ctrl = self.new_chat(contact, account, resource=resource, 1491 session=session) 1492 # last message is long time ago 1493 app.last_message_time[account][ctrl.get_full_jid()] = 0 1494 1495 win = ctrl.parent_win 1496 1497 win.set_active_tab(ctrl) 1498 1499 if app.connections[account].is_zeroconf and \ 1500 app.connections[account].status == 'offline': 1501 ctrl = win.get_control(fjid, account) 1502 if ctrl: 1503 ctrl.got_disconnected() 1504 1505################################################################################ 1506### Other Methods 1507################################################################################ 1508 1509 @staticmethod 1510 def create_account(account, 1511 username, 1512 domain, 1513 password, 1514 proxy_name, 1515 custom_host, 1516 anonymous=False): 1517 1518 account_label = f'{username}@{domain}' 1519 if anonymous: 1520 username = 'anon' 1521 account_label = f'anon@{domain}' 1522 1523 config = {} 1524 config['active'] = False 1525 config['name'] = username 1526 config['resource'] = 'gajim.%s' % helpers.get_random_string(8) 1527 config['account_label'] = account_label 1528 config['account_color'] = get_color_for_account( 1529 '%s@%s' % (username, domain)) 1530 config['hostname'] = domain 1531 config['savepass'] = True 1532 config['anonymous_auth'] = anonymous 1533 config['autoconnect'] = True 1534 config['sync_with_global_status'] = True 1535 1536 if proxy_name is not None: 1537 config['proxy'] = proxy_name 1538 1539 use_custom_host = custom_host is not None 1540 config['use_custom_host'] = use_custom_host 1541 if custom_host: 1542 host, _protocol, type_ = custom_host 1543 host, port = host.rsplit(':', maxsplit=1) 1544 config['custom_port'] = int(port) 1545 config['custom_host'] = host 1546 config['custom_type'] = type_.value 1547 1548 app.settings.add_account(account) 1549 for opt in config: 1550 app.settings.set_account_setting(account, opt, config[opt]) 1551 1552 # Password module depends on existing config 1553 passwords.save_password(account, password) 1554 1555 app.css_config.refresh() 1556 1557 # Action must be added before account window is updated 1558 app.app.add_account_actions(account) 1559 1560 window = get_app_window('AccountsWindow') 1561 if window is not None: 1562 window.add_account(account) 1563 1564 def enable_account(self, account): 1565 if account == app.ZEROCONF_ACC_NAME: 1566 app.connections[account] = connection_zeroconf.ConnectionZeroconf( 1567 account) 1568 else: 1569 app.connections[account] = Client(account) 1570 1571 app.plugin_manager.register_modules_for_account( 1572 app.connections[account]) 1573 1574 # update variables 1575 self.instances[account] = { 1576 'infos': {}, 'disco': {}, 'gc_config': {}, 'search': {}, 1577 'sub_request': {}} 1578 self.minimized_controls[account] = {} 1579 app.groups[account] = {} 1580 app.contacts.add_account(account) 1581 app.gc_connected[account] = {} 1582 app.automatic_rooms[account] = {} 1583 app.newly_added[account] = [] 1584 app.to_be_removed[account] = [] 1585 if account == app.ZEROCONF_ACC_NAME: 1586 app.nicks[account] = app.ZEROCONF_ACC_NAME 1587 else: 1588 app.nicks[account] = app.settings.get_account_setting(account, 1589 'name') 1590 app.block_signed_in_notifications[account] = True 1591 app.last_message_time[account] = {} 1592 # refresh roster 1593 if len(app.connections) >= 2: 1594 # Do not merge accounts if only one exists 1595 self.roster.regroup = app.settings.get('mergeaccounts') 1596 else: 1597 self.roster.regroup = False 1598 self.roster.setup_and_draw_roster() 1599 gui_menu_builder.build_accounts_menu() 1600 self.roster.send_status(account, 'online', '') 1601 app.settings.set_account_setting(account, 'active', True) 1602 app.app.update_app_actions_state() 1603 window = get_app_window('AccountsWindow') 1604 if window is not None: 1605 GLib.idle_add(window.enable_account, account, True) 1606 1607 def disable_account(self, account): 1608 self.roster.close_all(account, force=True) 1609 for jid in self.minimized_controls[account]: 1610 ctrl = self.minimized_controls[account][jid] 1611 ctrl.shutdown() 1612 1613 for win in get_app_windows(account): 1614 # Close all account specific windows, except the RemoveAccount 1615 # dialog. It shows if the removal was successful. 1616 if type(win).__name__ == 'RemoveAccount': 1617 continue 1618 win.destroy() 1619 1620 if account == app.ZEROCONF_ACC_NAME: 1621 app.connections[account].disable_account() 1622 app.connections[account].cleanup() 1623 del app.connections[account] 1624 del self.instances[account] 1625 del self.minimized_controls[account] 1626 del app.nicks[account] 1627 del app.block_signed_in_notifications[account] 1628 del app.groups[account] 1629 app.contacts.remove_account(account) 1630 del app.gc_connected[account] 1631 del app.automatic_rooms[account] 1632 del app.to_be_removed[account] 1633 del app.newly_added[account] 1634 del app.last_message_time[account] 1635 if len(app.connections) >= 2: 1636 # Do not merge accounts if only one exists 1637 self.roster.regroup = app.settings.get('mergeaccounts') 1638 else: 1639 self.roster.regroup = False 1640 app.settings.set_account_setting(account, 'roster_version', '') 1641 self.roster.setup_and_draw_roster() 1642 self.roster.update_status_selector() 1643 gui_menu_builder.build_accounts_menu() 1644 app.settings.set_account_setting(account, 'active', False) 1645 app.app.update_app_actions_state() 1646 1647 def remove_account(self, account): 1648 if app.settings.get_account_setting(account, 'active'): 1649 self.disable_account(account) 1650 1651 app.storage.cache.remove_roster(account) 1652 # Delete password must be before del_per() because it calls set_per() 1653 # which would recreate the account with defaults values if not found 1654 passwords.delete_password(account) 1655 app.settings.remove_account(account) 1656 app.app.remove_account_actions(account) 1657 1658 window = get_app_window('AccountsWindow') 1659 if window is not None: 1660 window.remove_account(account) 1661 1662 def autoconnect(self): 1663 """ 1664 Auto connect at startup 1665 """ 1666 1667 for account in app.connections: 1668 if not app.settings.get_account_setting(account, 'autoconnect'): 1669 continue 1670 1671 status = 'online' 1672 status_message = '' 1673 1674 if app.settings.get_account_setting(account, 'restore_last_status'): 1675 status = app.settings.get_account_setting(account, 'last_status') 1676 status_message = app.settings.get_account_setting( 1677 account, 'last_status_msg') 1678 status_message = helpers.from_one_line(status_message) 1679 1680 self.roster.send_status(account, status, status_message) 1681 1682 def change_status(self, status=None): 1683 # status=None means we want to change the message only 1684 1685 ask = ask_for_status_message(status) 1686 1687 if status is None: 1688 status = helpers.get_global_show() 1689 1690 if ask: 1691 open_window('StatusChange', status=status) 1692 return 1693 1694 for account in app.connections: 1695 if not app.settings.get_account_setting(account, 1696 'sync_with_global_status'): 1697 continue 1698 1699 message = app.get_client(account).status_message 1700 self.roster.send_status(account, status, message) 1701 1702 def change_account_status(self, account, status=None): 1703 # status=None means we want to change the message only 1704 1705 ask = ask_for_status_message(status) 1706 1707 client = app.get_client(account) 1708 if status is None: 1709 status = client.status 1710 1711 if ask: 1712 open_window('StatusChange', status=status, account=account) 1713 return 1714 1715 message = client.status_message 1716 self.roster.send_status(account, status, message) 1717 1718 def show_systray(self): 1719 if not app.is_display(Display.WAYLAND): 1720 self.systray_enabled = True 1721 self.systray.show_icon() 1722 1723 def hide_systray(self): 1724 if not app.is_display(Display.WAYLAND): 1725 self.systray_enabled = False 1726 self.systray.hide_icon() 1727 1728 def process_connections(self): 1729 """ 1730 Called each foo (200) milliseconds. Check for idlequeue timeouts 1731 """ 1732 try: 1733 app.idlequeue.process() 1734 except Exception: 1735 # Otherwise, an exception will stop our loop 1736 1737 if sys.platform == 'win32': 1738 # On Windows process() calls select.select(), so we need this 1739 # executed as often as possible. 1740 # Adding it directly with GLib.idle_add() causes Gajim to use 1741 # too much CPU time. That's why its added with 1ms timeout. 1742 # On Linux only alarms are checked in process(), so we use 1743 # a bigger timeout 1744 timeout, in_seconds = 1, None 1745 else: 1746 timeout, in_seconds = app.idlequeue.PROCESS_TIMEOUT 1747 1748 if in_seconds: 1749 GLib.timeout_add_seconds(timeout, self.process_connections) 1750 else: 1751 GLib.timeout_add(timeout, self.process_connections) 1752 raise 1753 return True # renew timeout (loop for ever) 1754 1755 @staticmethod 1756 def save_config(): 1757 app.settings.save() 1758 1759 def update_avatar(self, account=None, jid=None, 1760 contact=None, room_avatar=False): 1761 self.avatar_storage.invalidate_cache(jid or contact.get_full_jid()) 1762 if room_avatar: 1763 app.nec.push_incoming_event( 1764 NetworkEvent('update-room-avatar', account=account, jid=jid)) 1765 elif contact is None: 1766 app.nec.push_incoming_event( 1767 NetworkEvent('update-roster-avatar', account=account, jid=jid)) 1768 else: 1769 app.nec.push_incoming_event(NetworkEvent('update-gc-avatar', 1770 contact=contact, 1771 room_jid=contact.room_jid)) 1772 1773 def save_avatar(self, data): 1774 return self.avatar_storage.save_avatar(data) 1775 1776 def get_avatar(self, contact, size, scale, show=None, pixbuf=False): 1777 if pixbuf: 1778 return self.avatar_storage.get_pixbuf(contact, size, scale, show) 1779 return self.avatar_storage.get_surface(contact, size, scale, show) 1780 1781 def avatar_exists(self, filename): 1782 return self.avatar_storage.get_avatar_path(filename) is not None 1783 1784 # does JID exist only within a groupchat? 1785 def is_pm_contact(self, fjid, account): 1786 bare_jid = app.get_jid_without_resource(fjid) 1787 1788 gc_ctrl = self.msg_win_mgr.get_gc_control(bare_jid, account) 1789 1790 if not gc_ctrl and \ 1791 bare_jid in self.minimized_controls[account]: 1792 gc_ctrl = self.minimized_controls[account][bare_jid] 1793 1794 return gc_ctrl and gc_ctrl.is_groupchat 1795 1796 @staticmethod 1797 def create_ipython_window(): 1798 # Check if IPython is installed 1799 ipython = find_spec('IPython') 1800 is_installed = ipython is not None 1801 if not is_installed: 1802 # Abort early to avoid tracebacks 1803 print('IPython is not installed') 1804 return 1805 try: 1806 from gajim.dev.ipython_view import IPythonView 1807 except ImportError: 1808 print('ipython_view not found') 1809 return 1810 from gi.repository import Pango 1811 1812 if os.name == 'nt': 1813 font = 'Lucida Console 9' 1814 else: 1815 font = 'Luxi Mono 10' 1816 1817 window = Gtk.Window() 1818 window.set_title(_('Gajim: IPython Console')) 1819 window.set_size_request(750, 550) 1820 window.set_resizable(True) 1821 sw = Gtk.ScrolledWindow() 1822 sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 1823 view = IPythonView() 1824 view.override_font(Pango.FontDescription(font)) 1825 view.set_wrap_mode(Gtk.WrapMode.CHAR) 1826 sw.add(view) 1827 window.add(sw) 1828 window.show_all() 1829 def on_delete(win, event): 1830 win.hide() 1831 return True 1832 window.connect('delete_event', on_delete) 1833 view.updateNamespace({'gajim': app}) 1834 app.ipython_window = window 1835 1836 def _network_status_changed(self, monitor, _param): 1837 connected = monitor.get_network_available() 1838 if connected == self.network_state: 1839 return 1840 1841 self.network_state = connected 1842 if connected: 1843 log.info('Network connection available') 1844 else: 1845 log.info('Network connection lost') 1846 for connection in app.connections.values(): 1847 if (connection.state.is_connected or 1848 connection.state.is_available): 1849 connection.disconnect(gracefully=False, reconnect=True) 1850 1851 def create_zeroconf_default_config(self): 1852 if app.settings.get_account_setting(app.ZEROCONF_ACC_NAME, 'name'): 1853 return 1854 log.info('Creating zeroconf account') 1855 app.settings.add_account(app.ZEROCONF_ACC_NAME) 1856 app.settings.set_account_setting(app.ZEROCONF_ACC_NAME, 1857 'autoconnect', 1858 True) 1859 app.settings.set_account_setting(app.ZEROCONF_ACC_NAME, 1860 'no_log_for', 1861 '') 1862 app.settings.set_account_setting(app.ZEROCONF_ACC_NAME, 1863 'password', 1864 'zeroconf') 1865 app.settings.set_account_setting(app.ZEROCONF_ACC_NAME, 1866 'sync_with_global_status', 1867 True) 1868 app.settings.set_account_setting(app.ZEROCONF_ACC_NAME, 1869 'custom_port', 1870 5298) 1871 app.settings.set_account_setting(app.ZEROCONF_ACC_NAME, 1872 'is_zeroconf', 1873 True) 1874 app.settings.set_account_setting(app.ZEROCONF_ACC_NAME, 1875 'use_ft_proxies', 1876 False) 1877 app.settings.set_account_setting(app.ZEROCONF_ACC_NAME, 1878 'active', 1879 False) 1880 1881 def check_for_updates(self): 1882 if not app.settings.get('check_for_update'): 1883 return 1884 1885 now = datetime.now() 1886 last_check = app.settings.get('last_update_check') 1887 if not last_check: 1888 def _on_cancel(): 1889 app.settings.set('check_for_update', False) 1890 1891 def _on_check(): 1892 self._get_latest_release() 1893 1894 ConfirmationDialog( 1895 _('Update Check'), 1896 _('Gajim Update Check'), 1897 _('Search for Gajim updates periodically?'), 1898 [DialogButton.make('Cancel', 1899 text=_('_No'), 1900 callback=_on_cancel), 1901 DialogButton.make('Accept', 1902 text=_('_Search Periodically'), 1903 callback=_on_check)]).show() 1904 return 1905 1906 last_check_time = datetime.strptime(last_check, '%Y-%m-%d %H:%M') 1907 if (now - last_check_time).days < 7: 1908 return 1909 1910 self._get_latest_release() 1911 1912 def _get_latest_release(self): 1913 log.info('Checking for Gajim updates') 1914 session = Soup.Session() 1915 session.props.user_agent = 'Gajim %s' % app.version 1916 message = Soup.Message.new('GET', 'https://gajim.org/current-version.json') 1917 session.queue_message(message, self._on_update_checked) 1918 1919 def _on_update_checked(self, _session, message): 1920 now = datetime.now() 1921 app.settings.set('last_update_check', now.strftime('%Y-%m-%d %H:%M')) 1922 1923 body = message.props.response_body.data 1924 if not body: 1925 log.warning('Could not reach gajim.org for update check') 1926 return 1927 1928 data = json.loads(body) 1929 latest_version = data['current_version'] 1930 1931 if V(latest_version) > V(app.version): 1932 def _on_cancel(is_checked): 1933 if is_checked: 1934 app.settings.set('check_for_update', False) 1935 1936 def _on_update(is_checked): 1937 if is_checked: 1938 app.settings.set('check_for_update', False) 1939 helpers.open_uri('https://gajim.org/download') 1940 1941 ConfirmationCheckDialog( 1942 _('Update Available'), 1943 _('Gajim Update Available'), 1944 _('There is an update available for Gajim ' 1945 '(latest version: %s)') % str(latest_version), 1946 _('_Do not show again'), 1947 [DialogButton.make('Cancel', 1948 text=_('_Later'), 1949 callback=_on_cancel), 1950 DialogButton.make('Accept', 1951 text=_('_Update Now'), 1952 callback=_on_update)]).show() 1953 else: 1954 log.info('Gajim is up to date') 1955 1956 def run(self, application): 1957 if app.settings.get('trayicon') != 'never': 1958 self.show_systray() 1959 1960 self.roster = roster_window.RosterWindow(application) 1961 if self.msg_win_mgr.mode == \ 1962 MessageWindowMgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: 1963 self.msg_win_mgr.create_window(None, None, None) 1964 1965 # Creating plugin manager 1966 from gajim import plugins 1967 app.plugin_manager = plugins.PluginManager() 1968 app.plugin_manager.init_plugins() 1969 1970 self.roster._before_fill() 1971 for account in app.connections: 1972 app.connections[account].get_module('Roster').load_roster() 1973 self.roster._after_fill() 1974 1975 # get instances for windows/dialogs that will show_all()/hide() 1976 self.instances['file_transfers'] = FileTransfersWindow() 1977 1978 GLib.timeout_add(100, self.autoconnect) 1979 if sys.platform == 'win32': 1980 timeout, in_seconds = 20, None 1981 else: 1982 timeout, in_seconds = app.idlequeue.PROCESS_TIMEOUT 1983 1984 if in_seconds: 1985 GLib.timeout_add_seconds(timeout, self.process_connections) 1986 else: 1987 GLib.timeout_add(timeout, self.process_connections) 1988 1989 def remote_init(): 1990 if app.settings.get('remote_control'): 1991 try: 1992 from gajim import remote_control 1993 remote_control.GajimRemote() 1994 except Exception: 1995 pass 1996 GLib.timeout_add_seconds(5, remote_init) 1997 1998 def __init__(self): 1999 app.interface = self 2000 app.thread_interface = ThreadInterface 2001 # This is the manager and factory of message windows set by the module 2002 self.msg_win_mgr = None 2003 self.minimized_controls = {} 2004 self.pass_dialog = {} 2005 self.db_error_dialog = None 2006 2007 self.handlers = {} 2008 self.roster = None 2009 2010 self.avatar_storage = AvatarStorage() 2011 2012 # Load CSS files 2013 app.load_css_config() 2014 2015 app.storage.archive.reset_shown_unread_messages() 2016 2017 for account in app.settings.get_accounts(): 2018 if app.settings.get_account_setting(account, 'is_zeroconf'): 2019 app.ZEROCONF_ACC_NAME = account 2020 break 2021 2022 app.idlequeue = idlequeue.get_idlequeue() 2023 # resolve and keep current record of resolved hosts 2024 app.socks5queue = socks5.SocksQueue(app.idlequeue, 2025 self.handle_event_file_rcv_completed, 2026 self.handle_event_file_progress, 2027 self.handle_event_file_error) 2028 app.proxy65_manager = proxy65_manager.Proxy65Manager(app.idlequeue) 2029 app.default_session_type = ChatControlSession 2030 2031 # Creating Network Events Controller 2032 from gajim.common import nec 2033 app.nec = nec.NetworkEventsController() 2034 app.notification = Notification() 2035 2036 self.create_core_handlers_list() 2037 self.register_core_handlers() 2038 2039 # self.create_zeroconf_default_config() 2040 # if app.settings.get_account_setting(app.ZEROCONF_ACC_NAME, 'active') \ 2041 # and app.is_installed('ZEROCONF'): 2042 # app.connections[app.ZEROCONF_ACC_NAME] = \ 2043 # connection_zeroconf.ConnectionZeroconf(app.ZEROCONF_ACC_NAME) 2044 2045 for account in app.settings.get_accounts(): 2046 if (not app.settings.get_account_setting(account, 'is_zeroconf') and 2047 app.settings.get_account_setting(account, 'active')): 2048 app.connections[account] = Client(account) 2049 2050 self.instances = {} 2051 2052 for a in app.connections: 2053 self.instances[a] = {'infos': {}, 'disco': {}, 'gc_config': {}, 2054 'search': {}, 'sub_request': {}} 2055 self.minimized_controls[a] = {} 2056 app.contacts.add_account(a) 2057 app.groups[a] = {} 2058 app.gc_connected[a] = {} 2059 app.automatic_rooms[a] = {} 2060 app.newly_added[a] = [] 2061 app.to_be_removed[a] = [] 2062 app.nicks[a] = app.settings.get_account_setting(a, 'name') 2063 app.block_signed_in_notifications[a] = True 2064 app.last_message_time[a] = {} 2065 2066 if sys.platform not in ('win32', 'darwin'): 2067 logind.enable() 2068 music_track.enable() 2069 else: 2070 GLib.timeout_add_seconds(20, self.check_for_updates) 2071 2072 idle.Monitor.set_interval(app.settings.get('autoawaytime') * 60, 2073 app.settings.get('autoxatime') * 60) 2074 2075 self.systray_enabled = False 2076 2077 if not app.is_display(Display.WAYLAND): 2078 from gajim.gui import statusicon 2079 self.systray = statusicon.StatusIcon() 2080 2081 if sys.platform in ('win32', 'darwin'): 2082 from gajim.gui.emoji_chooser import emoji_chooser 2083 emoji_chooser.load() 2084 2085 self.last_ftwindow_update = 0 2086 2087 self._network_monitor = Gio.NetworkMonitor.get_default() 2088 self._network_monitor.connect('notify::network-available', 2089 self._network_status_changed) 2090 self.network_state = self._network_monitor.get_network_available() 2091 2092 2093class ThreadInterface: 2094 def __init__(self, func, func_args=(), callback=None, callback_args=()): 2095 """ 2096 Call a function in a thread 2097 """ 2098 def thread_function(func, func_args, callback, callback_args): 2099 output = func(*func_args) 2100 if callback: 2101 GLib.idle_add(callback, output, *callback_args) 2102 2103 Thread(target=thread_function, args=(func, func_args, callback, 2104 callback_args)).start() 2105