Commit 2aa132a3 authored by Dan Pascu's avatar Dan Pascu

Added dialog for adding and creating SIP accounts

parent e3110d79
......@@ -8,19 +8,26 @@ __version__ = '0.1.0'
__date__ = 'July 5, 2010'
import os
import sys
from collections import defaultdict
import cjson
from PyQt4.QtCore import QThread
from PyQt4.QtGui import QApplication
from application import log
from application.notification import IObserver, NotificationCenter
from application.python.util import Null
from application.system import unlink
from gnutls.crypto import X509Certificate, X509PrivateKey
from gnutls.errors import GNUTLSError
from zope.interface import implements
from sipsimple.account import Account, BonjourAccount
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.configuration.backend.file import FileBackend
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.util import makedirs
from blink.configuration.account import AccountExtension, BonjourAccountExtension
from blink.configuration.settings import SIPSimpleSettingsExtension
......@@ -84,6 +91,81 @@ class Blink(QApplication):
log_manager = LogManager()
log_manager.stop()
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)
account.display_name = data['display_name']
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']
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
def customEvent(self, event):
handler = getattr(self, '_EH_%s' % event.name, Null)
handler(event)
......@@ -105,6 +187,10 @@ class Blink(QApplication):
@run_in_gui_thread
def _NH_SIPApplicationDidStart(self, notification):
account_manager = AccountManager()
self.fetch_account()
if account_manager.get_accounts() == [BonjourAccount()]:
self.main_window.add_account_dialog.open_for_create()
self.main_window.show()
self.update_manager.initialize()
......
This diff is collapsed.
......@@ -5,24 +5,34 @@
__all__ = ['AccountExtension', 'BonjourAccountExtension']
from sipsimple.account import PSTNSettings
from sipsimple.account import PSTNSettings, TLSSettings
from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension
from sipsimple.util import user_info
from blink.configuration.datatypes import CustomSoundFile, DefaultPath
from blink.configuration.datatypes import ApplicationDataPath, CustomSoundFile, DefaultPath, HTTPURL
class PSTNSettingsExtension(PSTNSettings):
idd_prefix = Setting(type=unicode, default=None, nillable=True)
class ServerSettings(SettingsGroup):
settings_url = Setting(type=HTTPURL, default=None, nillable=True)
class SoundSettings(SettingsGroup):
inbound_ringtone = Setting(type=CustomSoundFile, default=CustomSoundFile(DefaultPath), nillable=True)
class TLSSettingsExtension(TLSSettings):
certificate = Setting(type=ApplicationDataPath, default=None, nillable=True)
class AccountExtension(SettingsObjectExtension):
pstn = PSTNSettingsExtension
server = ServerSettings
sounds = SoundSettings
tls = TLSSettingsExtension
display_name = Setting(type=str, default=user_info.fullname, nillable=True)
......
......@@ -3,10 +3,13 @@
"""Definitions of datatypes for use in settings extensions."""
__all__ = ['ApplicationDataPath', 'SoundFile', 'DefaultPath', 'CustomSoundFile']
__all__ = ['ApplicationDataPath', 'SoundFile', 'DefaultPath', 'CustomSoundFile', 'HTTPURL']
import os
import re
from urlparse import urlparse
from sipsimple.configuration.datatypes import Hostname
from blink.resources import ApplicationData
......@@ -98,3 +101,15 @@ class CustomSoundFile(object):
del _get_path, _set_path
class HTTPURL(unicode):
def __new__(cls, value):
value = unicode(value)
url = urlparse(value)
if url.scheme not in (u'http', u'https'):
raise ValueError("illegal HTTP URL scheme (http and https only): %s" % url.scheme)
Hostname(url.hostname)
if url.port is not None and not (0 < url.port < 65536):
raise ValueError("illegal port value: %d" % url.port)
return value
......@@ -9,10 +9,10 @@ import platform
import sys
from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension
from sipsimple.configuration.settings import AudioSettings, LogsSettings
from sipsimple.configuration.settings import AudioSettings, LogsSettings, TLSSettings
from blink import __version__
from blink.configuration.datatypes import ApplicationDataPath, SoundFile
from blink.configuration.datatypes import ApplicationDataPath, HTTPURL, SoundFile
from blink.resources import Resources
......@@ -27,15 +27,25 @@ class LogsSettingsExtension(LogsSettings):
trace_notifications = Setting(type=bool, default=False)
class ServerSettings(SettingsGroup):
enrollment_url = Setting(type=HTTPURL, default="https://blink.sipthor.net/enrollment.phtml")
class SoundSettings(SettingsGroup):
inbound_ringtone = Setting(type=SoundFile, default=SoundFile(Resources.get('sounds/inbound_ringtone.wav')), nillable=True)
outbound_ringtone = Setting(type=SoundFile, default=SoundFile(Resources.get('sounds/outbound_ringtone.wav')), nillable=True)
class TLSSettingsExtension(TLSSettings):
ca_list = Setting(type=ApplicationDataPath, default=None, nillable=True)
class SIPSimpleSettingsExtension(SettingsObjectExtension):
audio = AudioSettingsExtension
logs = LogsSettingsExtension
server = ServerSettings
sounds = SoundSettings
tls = TLSSettingsExtension
user_agent = Setting(type=str, default='Blink %s (%s)' % (__version__, platform.system() if sys.platform!='darwin' else 'MacOSX Qt'))
......
......@@ -20,7 +20,7 @@ from sipsimple.application import SIPApplication
from sipsimple.configuration.settings import SIPSimpleSettings
from blink.aboutpanel import AboutPanel
from blink.accounts import AccountModel, ActiveAccountModel
from blink.accounts import AccountModel, ActiveAccountModel, AddAccountDialog
from blink.contacts import BonjourNeighbour, Contact, ContactGroup, ContactEditorDialog, ContactModel, ContactSearchModel
from blink.sessions import SessionManager, SessionModel
from blink.resources import Resources
......@@ -62,6 +62,7 @@ class MainWindow(base_class, ui_class):
self.contact_model.load()
self.about_panel = AboutPanel(self)
self.add_account_dialog = AddAccountDialog(self)
self.contact_editor_dialog = ContactEditorDialog(self.contact_model, self)
self.session_model = SessionModel(self)
......@@ -112,6 +113,7 @@ class MainWindow(base_class, ui_class):
self.search_box.shortcut.activated.connect(self.search_box.setFocus)
self.about_action.triggered.connect(self.about_panel.show)
self.add_account_action.triggered.connect(self.add_account_dialog.open_for_add)
self.mute_action.triggered.connect(self._SH_MuteButtonClicked)
self.redial_action.triggered.connect(self._SH_RedialActionTriggered)
self.silent_action.triggered.connect(self._SH_SilentButtonClicked)
......@@ -168,6 +170,7 @@ class MainWindow(base_class, ui_class):
def closeEvent(self, event):
super(MainWindow, self).closeEvent(event)
self.about_panel.close()
self.add_account_dialog.close()
self.contact_editor_dialog.close()
def set_user_icon(self, image_file_name):
......
# Copyright (c) 2010 AG Projects. See LICENSE for details.
#
from __future__ import with_statement
__all__ = ['BackgroundFrame']
from PyQt4.QtCore import Qt, QEvent, QPoint, QRect, QSize
from PyQt4.QtGui import QColor, QFrame, QPainter, QPixmap
from blink.resources import Resources
from blink.widgets.util import QtDynamicProperty
class BackgroundFrame(QFrame):
backgroundColor = QtDynamicProperty('backgroundColor', unicode)
backgroundImage = QtDynamicProperty('backgroundImage', unicode)
imageGeometry = QtDynamicProperty('imageGeometry', QRect)
def __init__(self, parent=None):
super(BackgroundFrame, self).__init__(parent)
self.backgroundColor = None
self.backgroundImage = None
self.imageGeometry = None
self.pixmap = None
self.scaled_pixmap = None
@property
def image_position(self):
return QPoint(0, 0) if self.imageGeometry is None else self.imageGeometry.topLeft()
@property
def image_size(self):
if self.imageGeometry is not None:
size = self.imageGeometry.size().expandedTo(QSize(0, 0)) # requested size with negative values turned to 0
if size.isNull():
return size if self.pixmap is None else self.pixmap.size()
elif size.width() == 0:
return size.expandedTo(QSize(16777215, 0))
elif size.height() == 0:
return size.expandedTo(QSize(0, 16777215))
else:
return size
elif self.pixmap:
return self.pixmap.size()
else:
return QSize(0, 0)
def event(self, event):
if event.type() == QEvent.DynamicPropertyChange:
if event.propertyName() == 'backgroundImage':
self.pixmap = QPixmap()
if self.backgroundImage and self.pixmap.load(Resources.get(self.backgroundImage)):
self.scaled_pixmap = self.pixmap.scaled(self.image_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
else:
self.pixmap = self.scaled_pixmap = None
self.update()
elif event.propertyName() == 'imageGeometry' and self.pixmap:
self.scaled_pixmap = self.pixmap.scaled(self.image_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.update()
elif event.propertyName() == 'backgroundColor':
self.update()
return super(BackgroundFrame, self).event(event)
def resizeEvent(self, event):
self.scaled_pixmap = self.pixmap and self.pixmap.scaled(self.image_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
def paintEvent(self, event):
super(BackgroundFrame, self).paintEvent(event)
painter = QPainter(self)
if self.backgroundColor:
painter.fillRect(self.rect(), QColor(self.backgroundColor))
if self.scaled_pixmap is not None:
painter.drawPixmap(self.image_position, self.scaled_pixmap)
painter.end()
# Copyright (c) 2010 AG Projects. See LICENSE for details.
#
__all__ = ['LineEdit', 'ValidatingLineEdit']
import re
from PyQt4.QtCore import Qt, QEvent, pyqtSignal
from PyQt4.QtGui import QLineEdit, QBoxLayout, QHBoxLayout, QLayout, QPainter, QPalette, QSpacerItem, QSizePolicy, QStyle, QWidget, QStyleOptionFrameV2
from PyQt4.QtGui import QLineEdit, QBoxLayout, QHBoxLayout, QLabel, QLayout, QPainter, QPalette, QPixmap, QSpacerItem, QSizePolicy, QStyle, QWidget, QStyleOptionFrameV2
from blink.resources import Resources
from blink.widgets.util import QtDynamicProperty
......@@ -117,3 +122,46 @@ class LineEdit(QLineEdit):
widget.hide()
class ValidatingLineEdit(LineEdit):
statusChanged = pyqtSignal()
def __init__(self, parent=None):
super(ValidatingLineEdit, self).__init__(parent)
self.invalid_entry_label = QLabel(self)
self.invalid_entry_label.setFixedSize(18, 16)
self.invalid_entry_label.setPixmap(QPixmap(Resources.get('icons/invalid16.png')))
self.invalid_entry_label.setScaledContents(False)
self.invalid_entry_label.setAlignment(Qt.AlignCenter)
self.invalid_entry_label.setObjectName('invalid_entry_label')
self.invalid_entry_label.hide()
self.addTailWidget(self.invalid_entry_label)
option = QStyleOptionFrameV2()
self.initStyleOption(option)
frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth, option, self)
self.setMinimumHeight(self.invalid_entry_label.minimumHeight() + 2 + 2*frame_width)
self.textChanged.connect(self.text_changed)
self.valid = True
self.regexp = re.compile(r'.*')
def _get_regexp(self):
return self.__dict__['regexp']
def _set_regexp(self, regexp):
self.__dict__['regexp'] = regexp
valid = regexp.search(unicode(self.text())) is not None
if self.valid != valid:
self.invalid_entry_label.setVisible(not valid)
self.valid = valid
self.statusChanged.emit()
regexp = property(_get_regexp, _set_regexp)
del _get_regexp, _set_regexp
def text_changed(self, text):
valid = self.regexp.search(unicode(text)) is not None
if self.valid != valid:
self.invalid_entry_label.setVisible(not valid)
self.valid = valid
self.statusChanged.emit()
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment