__init__.py 11.5 KB
Newer Older
Dan Pascu's avatar
Dan Pascu committed
1 2 3 4 5
# Copyright (C) 2010 AG Projects. See LICENSE for details.
#

__all__ = ['Blink']

Dan Pascu's avatar
Dan Pascu committed
6

7 8
__version__ = '1.4.2'
__date__    = 'December 4th 2015'
Dan Pascu's avatar
Dan Pascu committed
9

10

11
import os
Dan Pascu's avatar
Dan Pascu committed
12
import sys
13
import sip
14
import cjson
15

16 17
sip.setapi('QString',  2)
sip.setapi('QVariant', 2)
18

19
from PyQt4.QtCore import Qt, QEvent
20 21
from PyQt4.QtGui  import QApplication

Dan Pascu's avatar
Dan Pascu committed
22 23
QApplication.setAttribute(Qt.AA_X11InitThreads, True)

24
from application import log
25
from application.notification import IObserver, NotificationCenter, NotificationData
26 27
from application.python import Null
from application.system import host, makedirs, unlink
28
from collections import defaultdict
29
from eventlib import api
30 31
from gnutls.crypto import X509Certificate, X509PrivateKey
from gnutls.errors import GNUTLSError
32 33
from zope.interface import implements

34
from sipsimple.account import Account, AccountManager, BonjourAccount
35
from sipsimple.addressbook import Contact, Group
36
from sipsimple.application import SIPApplication
37
from sipsimple.configuration.settings import SIPSimpleSettings
Dan Pascu's avatar
Dan Pascu committed
38
from sipsimple.payloads import XMLDocument
39
from sipsimple.storage import FileStorage
Dan Pascu's avatar
Dan Pascu committed
40 41
from sipsimple.threading import run_in_twisted_thread
from sipsimple.threading.green import run_in_green_thread
42

43 44 45 46 47
try:
    from blink import branding
except ImportError:
    branding = Null
from blink.chatwindow import ChatWindow
48
from blink.configuration.account import AccountExtension, BonjourAccountExtension
49
from blink.configuration.addressbook import ContactExtension, GroupExtension
50
from blink.configuration.datatypes import InvalidToken
51
from blink.configuration.settings import SIPSimpleSettingsExtension
52
from blink.logging import LogManager
Dan Pascu's avatar
Dan Pascu committed
53
from blink.mainwindow import MainWindow
54
from blink.presence import PresenceManager
Dan Pascu's avatar
Dan Pascu committed
55
from blink.resources import ApplicationData, Resources
56
from blink.sessions import SessionManager
57
from blink.update import UpdateManager
58
from blink.util import QSingleton, run_in_gui_thread
Dan Pascu's avatar
Dan Pascu committed
59

60

Dan Pascu's avatar
Dan Pascu committed
61
if hasattr(sys, 'frozen'):
62
    output = sys.stdout
Saul Ibarra's avatar
Saul Ibarra committed
63
    makedirs(ApplicationData.get('logs'))
64
    sys.stdout = sys.stderr = open(ApplicationData.get('logs/output.log'), 'a', 0)
65 66 67 68
    sys.stdout.write(output.getvalue())
    output.close()


Dan Pascu's avatar
Dan Pascu committed
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
class IPAddressMonitor(object):
    """
    An object which monitors the IP address used for the default route of the
    host and posts a SystemIPAddressDidChange notification when a change is
    detected.
    """

    def __init__(self):
        self.greenlet = None

    @run_in_green_thread
    def start(self):
        notification_center = NotificationCenter()

        if self.greenlet is not None:
            return
        self.greenlet = api.getcurrent()

        current_address = host.default_ip
        while True:
            new_address = host.default_ip
            # make sure the address stabilized
            api.sleep(5)
            if new_address != host.default_ip:
                continue
            if new_address != current_address:
95
                notification_center.post_notification(name='SystemIPAddressDidChange', sender=self, data=NotificationData(old_ip_address=current_address, new_ip_address=new_address))
Dan Pascu's avatar
Dan Pascu committed
96 97 98 99 100 101 102 103 104 105
                current_address = new_address
            api.sleep(5)

    @run_in_twisted_thread
    def stop(self):
        if self.greenlet is not None:
            api.kill(self.greenlet, api.GreenletExit())
            self.greenlet = None


106 107
class Blink(QApplication):
    __metaclass__ = QSingleton
Dan Pascu's avatar
Dan Pascu committed
108

109 110
    implements(IObserver)

Dan Pascu's avatar
Dan Pascu committed
111
    def __init__(self):
112
        super(Blink, self).__init__(sys.argv)
113
        self.setAttribute(Qt.AA_DontShowIconsInMenus, False)
Dan Pascu's avatar
Dan Pascu committed
114
        self.sip_application = SIPApplication()
115
        self.first_run = False
116 117 118 119 120 121

        self.setOrganizationDomain("ag-projects.com")
        self.setOrganizationName("AG Projects")
        self.setApplicationName("Blink")
        self.setApplicationVersion(__version__)

Dan Pascu's avatar
Dan Pascu committed
122
        self.main_window = MainWindow()
123
        self.chat_window = ChatWindow()
124 125 126 127
        self.main_window.__closed__ = True
        self.chat_window.__closed__ = True
        self.main_window.installEventFilter(self)
        self.chat_window.installEventFilter(self)
128

129 130 131 132 133 134 135 136 137 138
        self.main_window.addAction(self.chat_window.control_button.actions.main_window)
        self.chat_window.addAction(self.main_window.quit_action)
        self.chat_window.addAction(self.main_window.help_action)
        self.chat_window.addAction(self.main_window.redial_action)
        self.chat_window.addAction(self.main_window.join_conference_action)
        self.chat_window.addAction(self.main_window.mute_action)
        self.chat_window.addAction(self.main_window.silent_action)
        self.chat_window.addAction(self.main_window.preferences_action)
        self.chat_window.addAction(self.main_window.transfers_window_action)
        self.chat_window.addAction(self.main_window.logs_window_action)
Dan Pascu's avatar
Dan Pascu committed
139 140
        self.chat_window.addAction(self.main_window.received_files_window_action)
        self.chat_window.addAction(self.main_window.screenshots_window_action)
141

Dan Pascu's avatar
Dan Pascu committed
142
        self.ip_address_monitor = IPAddressMonitor()
143
        self.log_manager = LogManager()
144
        self.presence_manager = PresenceManager()
145 146
        self.session_manager = SessionManager()
        self.update_manager = UpdateManager()
Dan Pascu's avatar
Dan Pascu committed
147

148 149 150 151
        # Prevent application from exiting after last window is closed if system tray was initialized
        if self.main_window.system_tray_icon:
            self.setQuitOnLastWindowClosed(False)

152
        self.main_window.check_for_updates_action.triggered.connect(self.update_manager.check_for_updates)
153
        self.main_window.check_for_updates_action.setVisible(self.update_manager != Null)
154

Dan Pascu's avatar
Dan Pascu committed
155 156 157
        if getattr(sys, 'frozen', False):
            XMLDocument.schema_path = Resources.get('xml-schemas')

158 159
        Account.register_extension(AccountExtension)
        BonjourAccount.register_extension(BonjourAccountExtension)
160 161
        Contact.register_extension(ContactExtension)
        Group.register_extension(GroupExtension)
162 163
        SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)

164 165 166
        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=self.sip_application)

Saul Ibarra's avatar
Saul Ibarra committed
167 168
        branding.setup(self)

Dan Pascu's avatar
Dan Pascu committed
169
    def run(self):
170 171
        self.first_run = not os.path.exists(ApplicationData.get('config'))
        self.sip_application.start(FileStorage(ApplicationData.directory))
172
        self.exec_()
173
        self.update_manager.shutdown()
Dan Pascu's avatar
Dan Pascu committed
174 175
        self.sip_application.stop()
        self.sip_application.thread.join()
176
        self.log_manager.stop()
Dan Pascu's avatar
Dan Pascu committed
177

178 179 180 181 182
    def quit(self):
        self.chat_window.close()
        self.main_window.close()
        super(Blink, self).quit()

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
    def fetch_account(self):
        filename = os.path.expanduser('~/.blink_account')
        if not os.path.exists(filename):
            return
        try:
            data = open(filename).read()
            data = cjson.decode(data.replace(r'\/', '/'))
        except (OSError, IOError), e:
            print "Failed to read json data from ~/.blink_account: %s" % e
            return
        except cjson.DecodeError, e:
            print "Failed to decode json data from ~/.blink_account: %s" % e
            return
        finally:
            unlink(filename)
        data = defaultdict(lambda: None, data)
        account_id = data['sip_address']
        if account_id is None:
            return
        account_manager = AccountManager()
        try:
            account = account_manager.get_account(account_id)
        except KeyError:
            account = Account(account_id)
207
            account.display_name = data['display_name'] or None
208 209 210 211 212 213 214 215
            default_account = account
        else:
            default_account = account_manager.default_account
        account.auth.username = data['auth_username']
        account.auth.password = data['password'] or ''
        account.sip.outbound_proxy = data['outbound_proxy']
        account.xcap.xcap_root = data['xcap_root']
        account.nat_traversal.msrp_relay = data['msrp_relay']
216
        account.server.conference_server = data['conference_server']
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
        account.server.settings_url = data['settings_url']
        if data['passport'] is not None:
            try:
                passport = data['passport']
                certificate_path = self.save_certificates(account_id, passport['crt'], passport['key'], passport['ca'])
                account.tls.certificate = certificate_path
            except (GNUTLSError, IOError, OSError):
                pass
        account.enabled = True
        account.save()
        account_manager.default_account = default_account

    def save_certificates(self, sip_address, crt, key, ca):
        crt = crt.strip() + os.linesep
        key = key.strip() + os.linesep
        ca = ca.strip() + os.linesep
        X509Certificate(crt)
        X509PrivateKey(key)
        X509Certificate(ca)
        makedirs(ApplicationData.get('tls'))
        certificate_path = ApplicationData.get(os.path.join('tls', sip_address+'.crt'))
        file = open(certificate_path, 'w')
        os.chmod(certificate_path, 0600)
        file.write(crt+key)
        file.close()
        ca_path = ApplicationData.get(os.path.join('tls', 'ca.crt'))
        try:
            existing_cas = open(ca_path).read().strip() + os.linesep
        except:
            file = open(ca_path, 'w')
            file.write(ca)
            file.close()
        else:
            if ca not in existing_cas:
                file = open(ca_path, 'w')
                file.write(existing_cas+ca)
                file.close()
        settings = SIPSimpleSettings()
        settings.tls.ca_list = ca_path
        settings.save()
        return certificate_path

259 260 261 262 263 264 265 266 267 268 269 270 271
    def eventFilter(self, watched, event):
        if watched in (self.main_window, self.chat_window):
            if event.type() == QEvent.Show:
                watched.__closed__ = False
            elif event.type() == QEvent.Close:
                watched.__closed__ = True
                if self.main_window.__closed__ and self.chat_window.__closed__:
                    # close auxiliary windows
                    self.main_window.conference_dialog.close()
                    self.main_window.filetransfer_window.close()
                    self.main_window.preferences_window.close()
        return False

272 273 274 275 276 277 278 279 280 281 282
    def customEvent(self, event):
        handler = getattr(self, '_EH_%s' % event.name, Null)
        handler(event)

    def _EH_CallFunctionEvent(self, event):
        try:
            event.function(*event.args, **event.kw)
        except:
            log.error('Exception occured while calling function %s in the GUI thread' % event.function.__name__)
            log.err()

283 284 285 286
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

287
    def _NH_SIPApplicationWillStart(self, notification):
288
        self.log_manager.start()
289
        self.presence_manager.start()
290

291 292
    @run_in_gui_thread
    def _NH_SIPApplicationDidStart(self, notification):
Dan Pascu's avatar
Dan Pascu committed
293
        self.ip_address_monitor.start()
294
        self.fetch_account()
295
        self.main_window.show()
296
        settings = SIPSimpleSettings()
Dan Pascu's avatar
Dan Pascu committed
297 298
        accounts = AccountManager().get_accounts()
        if not accounts or (self.first_run and accounts==[BonjourAccount()]):
299
            self.main_window.preferences_window.show_create_account_dialog()
300 301
        if settings.google_contacts.authorization_token is InvalidToken:
            self.main_window.google_contacts_dialog.open_for_incorrect_password()
302
        self.update_manager.initialize()
303

Dan Pascu's avatar
Dan Pascu committed
304 305 306
    def _NH_SIPApplicationWillEnd(self, notification):
        self.ip_address_monitor.stop()

307 308 309
    def _NH_SIPApplicationDidEnd(self, notification):
        self.presence_manager.stop()

Dan Pascu's avatar
Dan Pascu committed
310