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' ...@@ -8,19 +8,26 @@ __version__ = '0.1.0'
__date__ = 'July 5, 2010' __date__ = 'July 5, 2010'
import os
import sys import sys
from collections import defaultdict
import cjson
from PyQt4.QtCore import QThread from PyQt4.QtCore import QThread
from PyQt4.QtGui import QApplication from PyQt4.QtGui import QApplication
from application import log from application import log
from application.notification import IObserver, NotificationCenter from application.notification import IObserver, NotificationCenter
from application.python.util import Null 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 zope.interface import implements
from sipsimple.account import Account, BonjourAccount from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication from sipsimple.application import SIPApplication
from sipsimple.configuration.backend.file import FileBackend from sipsimple.configuration.backend.file import FileBackend
from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.util import makedirs
from blink.configuration.account import AccountExtension, BonjourAccountExtension from blink.configuration.account import AccountExtension, BonjourAccountExtension
from blink.configuration.settings import SIPSimpleSettingsExtension from blink.configuration.settings import SIPSimpleSettingsExtension
...@@ -84,6 +91,81 @@ class Blink(QApplication): ...@@ -84,6 +91,81 @@ class Blink(QApplication):
log_manager = LogManager() log_manager = LogManager()
log_manager.stop() 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): def customEvent(self, event):
handler = getattr(self, '_EH_%s' % event.name, Null) handler = getattr(self, '_EH_%s' % event.name, Null)
handler(event) handler(event)
...@@ -105,6 +187,10 @@ class Blink(QApplication): ...@@ -105,6 +187,10 @@ class Blink(QApplication):
@run_in_gui_thread @run_in_gui_thread
def _NH_SIPApplicationDidStart(self, notification): 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.main_window.show()
self.update_manager.initialize() self.update_manager.initialize()
......
This diff is collapsed.
...@@ -5,24 +5,34 @@ ...@@ -5,24 +5,34 @@
__all__ = ['AccountExtension', 'BonjourAccountExtension'] __all__ = ['AccountExtension', 'BonjourAccountExtension']
from sipsimple.account import PSTNSettings from sipsimple.account import PSTNSettings, TLSSettings
from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension
from sipsimple.util import user_info 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): class PSTNSettingsExtension(PSTNSettings):
idd_prefix = Setting(type=unicode, default=None, nillable=True) idd_prefix = Setting(type=unicode, default=None, nillable=True)
class ServerSettings(SettingsGroup):
settings_url = Setting(type=HTTPURL, default=None, nillable=True)
class SoundSettings(SettingsGroup): class SoundSettings(SettingsGroup):
inbound_ringtone = Setting(type=CustomSoundFile, default=CustomSoundFile(DefaultPath), nillable=True) inbound_ringtone = Setting(type=CustomSoundFile, default=CustomSoundFile(DefaultPath), nillable=True)
class TLSSettingsExtension(TLSSettings):
certificate = Setting(type=ApplicationDataPath, default=None, nillable=True)
class AccountExtension(SettingsObjectExtension): class AccountExtension(SettingsObjectExtension):
pstn = PSTNSettingsExtension pstn = PSTNSettingsExtension
server = ServerSettings
sounds = SoundSettings sounds = SoundSettings
tls = TLSSettingsExtension
display_name = Setting(type=str, default=user_info.fullname, nillable=True) display_name = Setting(type=str, default=user_info.fullname, nillable=True)
......
...@@ -3,10 +3,13 @@ ...@@ -3,10 +3,13 @@
"""Definitions of datatypes for use in settings extensions.""" """Definitions of datatypes for use in settings extensions."""
__all__ = ['ApplicationDataPath', 'SoundFile', 'DefaultPath', 'CustomSoundFile'] __all__ = ['ApplicationDataPath', 'SoundFile', 'DefaultPath', 'CustomSoundFile', 'HTTPURL']
import os import os
import re import re
from urlparse import urlparse
from sipsimple.configuration.datatypes import Hostname
from blink.resources import ApplicationData from blink.resources import ApplicationData
...@@ -98,3 +101,15 @@ class CustomSoundFile(object): ...@@ -98,3 +101,15 @@ class CustomSoundFile(object):
del _get_path, _set_path 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 ...@@ -9,10 +9,10 @@ import platform
import sys import sys
from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension 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 import __version__
from blink.configuration.datatypes import ApplicationDataPath, SoundFile from blink.configuration.datatypes import ApplicationDataPath, HTTPURL, SoundFile
from blink.resources import Resources from blink.resources import Resources
...@@ -27,15 +27,25 @@ class LogsSettingsExtension(LogsSettings): ...@@ -27,15 +27,25 @@ class LogsSettingsExtension(LogsSettings):
trace_notifications = Setting(type=bool, default=False) 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): class SoundSettings(SettingsGroup):
inbound_ringtone = Setting(type=SoundFile, default=SoundFile(Resources.get('sounds/inbound_ringtone.wav')), nillable=True) 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) 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): class SIPSimpleSettingsExtension(SettingsObjectExtension):
audio = AudioSettingsExtension audio = AudioSettingsExtension
logs = LogsSettingsExtension logs = LogsSettingsExtension
server = ServerSettings
sounds = SoundSettings sounds = SoundSettings
tls = TLSSettingsExtension
user_agent = Setting(type=str, default='Blink %s (%s)' % (__version__, platform.system() if sys.platform!='darwin' else 'MacOSX Qt')) 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 ...@@ -20,7 +20,7 @@ from sipsimple.application import SIPApplication
from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.configuration.settings import SIPSimpleSettings
from blink.aboutpanel import AboutPanel 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.contacts import BonjourNeighbour, Contact, ContactGroup, ContactEditorDialog, ContactModel, ContactSearchModel
from blink.sessions import SessionManager, SessionModel from blink.sessions import SessionManager, SessionModel
from blink.resources import Resources from blink.resources import Resources
...@@ -62,6 +62,7 @@ class MainWindow(base_class, ui_class): ...@@ -62,6 +62,7 @@ class MainWindow(base_class, ui_class):
self.contact_model.load() self.contact_model.load()
self.about_panel = AboutPanel(self) self.about_panel = AboutPanel(self)
self.add_account_dialog = AddAccountDialog(self)
self.contact_editor_dialog = ContactEditorDialog(self.contact_model, self) self.contact_editor_dialog = ContactEditorDialog(self.contact_model, self)
self.session_model = SessionModel(self) self.session_model = SessionModel(self)
...@@ -112,6 +113,7 @@ class MainWindow(base_class, ui_class): ...@@ -112,6 +113,7 @@ class MainWindow(base_class, ui_class):
self.search_box.shortcut.activated.connect(self.search_box.setFocus) self.search_box.shortcut.activated.connect(self.search_box.setFocus)
self.about_action.triggered.connect(self.about_panel.show) 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.mute_action.triggered.connect(self._SH_MuteButtonClicked)
self.redial_action.triggered.connect(self._SH_RedialActionTriggered) self.redial_action.triggered.connect(self._SH_RedialActionTriggered)
self.silent_action.triggered.connect(self._SH_SilentButtonClicked) self.silent_action.triggered.connect(self._SH_SilentButtonClicked)
...@@ -168,6 +170,7 @@ class MainWindow(base_class, ui_class): ...@@ -168,6 +170,7 @@ class MainWindow(base_class, ui_class):
def closeEvent(self, event): def closeEvent(self, event):
super(MainWindow, self).closeEvent(event) super(MainWindow, self).closeEvent(event)
self.about_panel.close() self.about_panel.close()
self.add_account_dialog.close()
self.contact_editor_dialog.close() self.contact_editor_dialog.close()
def set_user_icon(self, image_file_name): 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. # Copyright (c) 2010 AG Projects. See LICENSE for details.
# #
__all__ = ['LineEdit', 'ValidatingLineEdit']
import re
from PyQt4.QtCore import Qt, QEvent, pyqtSignal 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 from blink.widgets.util import QtDynamicProperty
...@@ -117,3 +122,46 @@ class LineEdit(QLineEdit): ...@@ -117,3 +122,46 @@ class LineEdit(QLineEdit):
widget.hide() 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