Commit 8f5ac8a1 authored by Dan Pascu's avatar Dan Pascu

Modified code to store contacts in XCAP

parent 8a70ef47
......@@ -28,6 +28,7 @@ from gnutls.errors import GNUTLSError
from zope.interface import implements
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.addressbook import Contact, Group
from sipsimple.application import SIPApplication
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.storage import FileStorage
......@@ -35,6 +36,7 @@ from sipsimple.threading import run_in_twisted_thread
from sipsimple.threading.green import run_in_green_thread
from blink.configuration.account import AccountExtension, BonjourAccountExtension
from blink.configuration.addressbook import ContactExtension, GroupExtension
from blink.configuration.datatypes import InvalidToken
from blink.configuration.settings import SIPSimpleSettingsExtension
from blink.logging import LogManager
......@@ -101,6 +103,8 @@ class Blink(QApplication):
Account.register_extension(AccountExtension)
BonjourAccount.register_extension(BonjourAccountExtension)
Contact.register_extension(ContactExtension)
Group.register_extension(GroupExtension)
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
session_manager = SessionManager()
session_manager.initialize(self.main_window, self.main_window.session_model)
......
# Copyright (C) 2013 AG Projects. See LICENSE for details.
#
"""Blink addressbook settings extensions."""
__all__ = ['ContactExtension', 'GroupExtension']
from sipsimple.addressbook import ContactExtension, GroupExtension, SharedSetting
from sipsimple.configuration import Setting
from blink.configuration.datatypes import IconDescriptor
SharedSetting.set_namespace('ag-projects:blink')
class ContactExtension(ContactExtension):
#auto_answer = SharedSetting(type=bool, default=False)
default_uri = SharedSetting(type=str, nillable=True, default=None)
preferred_media = SharedSetting(type=str, default='audio')
icon = Setting(type=IconDescriptor, nillable=True, default=None)
class GroupExtension(GroupExtension):
position = Setting(type=int, nillable=True)
collapsed = Setting(type=bool, default=False)
......@@ -3,7 +3,7 @@
"""Definitions of datatypes for use in settings extensions."""
__all__ = ['ApplicationDataPath', 'SoundFile', 'DefaultPath', 'CustomSoundFile', 'HTTPURL', 'AuthorizationToken', 'InvalidToken']
__all__ = ['ApplicationDataPath', 'SoundFile', 'DefaultPath', 'CustomSoundFile', 'HTTPURL', 'AuthorizationToken', 'InvalidToken', 'IconDescriptor']
import os
import re
......@@ -136,3 +136,56 @@ class AuthorizationToken(str):
InvalidToken = AuthorizationToken() # a valid token is never empty
class IconDescriptor(object):
def __init__(self, url, etag=None):
self.url = url
self.etag = etag
def __getstate__(self):
if self.etag is None:
return unicode(self.url)
else:
return u'%s,%s' % (self.__dict__['url'], self.etag)
def __setstate__(self, state):
try:
url, etag = state.rsplit(u',', 1)
except ValueError:
self.__init__(state)
else:
self.__init__(url, etag)
def __eq__(self, other):
if isinstance(other, IconDescriptor):
return self.url==other.url and self.etag==other.etag
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.url, self.etag)
def _get_url(self):
url = self.__dict__['url']
file_scheme = 'file://'
if url.startswith(file_scheme):
url = file_scheme + ApplicationData.get(url[len(file_scheme):])
return url
def _set_url(self, url):
file_scheme = 'file://'
if url.startswith(file_scheme):
filename = os.path.normpath(url[len(file_scheme):])
if filename.startswith(ApplicationData.directory+os.path.sep):
filename = filename[len(ApplicationData.directory+os.path.sep):]
url = file_scheme + filename
self.__dict__['url'] = url
url = property(_get_url, _set_url)
del _get_url, _set_url
@property
def is_local(self):
return self.__dict__['url'].startswith('file://')
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -20,7 +20,7 @@ from sipsimple.configuration.settings import SIPSimpleSettings
from blink.aboutpanel import AboutPanel
from blink.accounts import AccountModel, ActiveAccountModel, ServerToolsAccountModel, ServerToolsWindow
from blink.contacts import BonjourNeighbour, Contact, ContactGroup, ContactEditorDialog, ContactModel, ContactSearchModel, GoogleContactsDialog
from blink.contacts import BonjourNeighbour, Contact, Group, ContactEditorDialog, ContactModel, ContactSearchModel, GoogleContactsDialog
from blink.preferences import PreferencesWindow
from blink.sessions import ConferenceDialog, SessionManager, SessionModel
from blink.configuration.datatypes import InvalidToken
......@@ -87,7 +87,7 @@ class MainWindow(base_class, ui_class):
# Windows, dialogs and panels
self.about_panel = AboutPanel(self)
self.conference_dialog = ConferenceDialog(self)
self.contact_editor_dialog = ContactEditorDialog(self.contact_model, self)
self.contact_editor_dialog = ContactEditorDialog(self)
self.google_contacts_dialog = GoogleContactsDialog(self)
self.preferences_window = PreferencesWindow(self.account_model, None)
self.server_tools_window = ServerToolsWindow(self.server_tools_account_model, None)
......@@ -163,8 +163,6 @@ class MainWindow(base_class, ui_class):
self.history_on_server_action.triggered.connect(self._AH_HistoryOnServer)
self.buy_pstn_access_action.triggered.connect(self._AH_PurchasePstnAccess)
self.contact_model.load()
def setupUi(self):
super(MainWindow, self).setupUi(self)
......@@ -341,15 +339,14 @@ class MainWindow(base_class, ui_class):
def _SH_AddContactButtonClicked(self, clicked):
model = self.contact_model
selected_items = ((index.row(), model.data(index)) for index in self.contact_list.selectionModel().selectedIndexes())
try:
item = (item for row, item in sorted(selected_items) if type(item) in (Contact, ContactGroup)).next()
preferred_group = item if type(item) is ContactGroup else item.group
except StopIteration:
try:
preferred_group = (group for group in model.contact_groups if type(group) is ContactGroup).next()
except StopIteration:
preferred_group = None
groups = set()
for index in self.contact_list.selectionModel().selectedIndexes():
item = model.data(index)
if isinstance(item, Group) and not item.virtual:
groups.add(item)
elif isinstance(item, Contact) and not item.group.virtual:
groups.add(item.group)
preferred_group = groups.pop() if len(groups)==1 else None
self.contact_editor_dialog.open_for_add(self.search_box.text(), preferred_group)
def _SH_AudioCallButtonClicked(self):
......@@ -359,7 +356,7 @@ class MainWindow(base_class, ui_class):
address = contact.uri or self.search_box.text()
name = contact.name or None
session_manager = SessionManager()
session_manager.start_call(name, address, contact=contact, account=BonjourAccount() if isinstance(contact, BonjourNeighbour) else None)
session_manager.start_call(name, address, contact=contact, account=BonjourAccount() if isinstance(contact.settings, BonjourNeighbour) else None)
def _SH_BreakConference(self):
active_session = self.session_model.data(self.session_list.selectionModel().selectedIndexes()[0])
......@@ -370,7 +367,7 @@ class MainWindow(base_class, ui_class):
if not isinstance(contact, Contact):
return
session_manager = SessionManager()
session_manager.start_call(contact.name, contact.uri, contact=contact, account=BonjourAccount() if isinstance(contact, BonjourNeighbour) else None)
session_manager.start_call(contact.name, contact.uri, contact=contact, account=BonjourAccount() if isinstance(contact.settings, BonjourNeighbour) else None)
def _SH_ContactListSelectionChanged(self, selected, deselected):
account_manager = AccountManager()
......
# Copyright (C) 2010 AG Projects. See LICENSE for details.
# Copyright (C) 2010-2013 AG Projects. See LICENSE for details.
#
"""Provide access to Blink's resources"""
__all__ = ['ApplicationData', 'Resources', 'IconCache']
__all__ = ['ApplicationData', 'Resources', 'IconManager']
import cPickle as pickle
import os
import platform
import sys
from PyQt4.QtCore import Qt
from PyQt4.QtGui import QPixmap
from application import log
from PyQt4.QtGui import QIcon, QPixmap
from application.python.descriptor import classproperty
from application.python.types import Singleton
from application.system import makedirs, unlink
from collections import deque
from hashlib import sha512
class DirectoryContextManager(unicode):
......@@ -78,127 +74,65 @@ class Resources(object):
return os.path.join(cls.directory, resource or u'')
class FileInfo(object):
def __init__(self, name):
self.name = name
try:
stat = os.stat(ApplicationData.get(os.path.join('images', name)))
except OSError:
self.mtime = None
self.size = None
else:
self.mtime = stat.st_mtime
self.size = stat.st_size
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return isinstance(other, FileInfo) and (self.name, self.mtime, self.size) == (other.name, other.mtime, other.size)
def __ne__(self, other):
return not self.__eq__(other)
class FileMapping(object):
def __init__(self, source, destination):
self.source = source
self.destination = destination
class IconCache(object):
class IconManager(object):
__metaclass__ = Singleton
max_size = 256
def __init__(self):
makedirs(ApplicationData.get('images'))
try:
self.filemap = pickle.load(open(ApplicationData.get(os.path.join('images', '.cached_icons.map'))))
except Exception:
self.filemap = {}
all_names = set('cached_icon_%04d.png' % x for x in xrange(1, 10000))
used_names = set(os.listdir(ApplicationData.get('images')))
self.available_names = deque(sorted(all_names - used_names))
def store(self, filename, pixmap=None):
if filename is None:
return None
if not os.path.isabs(filename):
return filename
if filename.startswith(ApplicationData.directory + os.path.sep):
return filename[len(ApplicationData.directory + os.path.sep):]
self.iconmap = {}
def get(self, id):
try:
file_mapping = self.filemap[filename]
return self.iconmap[id]
except KeyError:
pass
else:
source_info = FileInfo(filename)
destination_info = FileInfo(file_mapping.destination.name)
if (source_info, destination_info) == (file_mapping.source, file_mapping.destination):
return destination_info.name
try:
destination_name = os.path.join('images', self.available_names.popleft())
except IndexError:
# No more available file names. Return original file for now
return filename
if pixmap is None:
pixmap = QPixmap()
filename = ApplicationData.get(os.path.join('images', id + '.png'))
if pixmap.load(filename):
pixmap = pixmap.scaled(32, 32, Qt.KeepAspectRatio, Qt.SmoothTransformation)
makedirs(ApplicationData.get('images'))
if pixmap.save(ApplicationData.get(destination_name)):
source_info = FileInfo(filename)
destination_info = FileInfo(destination_name)
file_mapping = FileMapping(source_info, destination_info)
self.filemap[filename] = file_mapping
map_filename = ApplicationData.get(os.path.join('images', '.cached_icons.map'))
map_tempname = map_filename + '.tmp'
try:
file = open(map_tempname, 'wb')
pickle.dump(self.filemap, file)
file.close()
if sys.platform == 'win32':
unlink(map_filename)
os.rename(map_tempname, map_filename)
except Exception, e:
log.error("could not save icon cache file mappings: %s" % e)
return destination_name
icon = QIcon(pixmap)
icon.filename = filename
else:
self.available_names.appendleft(os.path.basename(destination_name))
return filename
icon = None
return self.iconmap.setdefault(id, icon)
def store_image(self, data):
if data is None:
return None
data_hash = sha512(data).hexdigest()
try:
return self.filemap[data_hash].destination
except KeyError:
pass
try:
destination_name = os.path.join('images', self.available_names.popleft())
except IndexError:
# No more available file names.
return None
def store_data(self, id, data):
directory = ApplicationData.get('images')
filename = os.path.join(directory, id + '.png')
makedirs(directory)
pixmap = QPixmap()
if pixmap.loadFromData(data):
pixmap = pixmap.scaled(32, 32, Qt.KeepAspectRatio, Qt.SmoothTransformation)
makedirs(ApplicationData.get('images'))
if pixmap.save(ApplicationData.get(destination_name)):
file_mapping = FileMapping(data_hash, destination_name)
self.filemap[data_hash] = file_mapping
map_filename = ApplicationData.get(os.path.join('images', '.cached_icons.map'))
map_tempname = map_filename + '.tmp'
try:
file = open(map_tempname, 'wb')
pickle.dump(self.filemap, file)
file.close()
if sys.platform == 'win32':
unlink(map_filename)
os.rename(map_tempname, map_filename)
except Exception, e:
log.error("could not save icon cache file mappings: %s" % e)
return destination_name
if data is not None and pixmap.loadFromData(data):
if pixmap.size().width() > self.max_size or pixmap.size().height() > self.max_size:
pixmap = pixmap.scaled(self.max_size, self.max_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
pixmap.save(filename)
icon = QIcon(pixmap)
icon.filename = filename
else:
unlink(filename)
icon = None
self.iconmap[id] = icon
return icon
def store_file(self, id, file):
directory = ApplicationData.get('images')
filename = os.path.join(directory, id + '.png')
if filename == os.path.normpath(file):
return self.iconmap.get(id, None)
makedirs(directory)
pixmap = QPixmap()
if file is not None and pixmap.load(file):
if pixmap.size().width() > self.max_size or pixmap.size().height() > self.max_size:
pixmap = pixmap.scaled(self.max_size, self.max_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
pixmap.save(filename)
icon = QIcon(pixmap)
icon.filename = filename
else:
self.available_names.appendleft(os.path.basename(destination_name))
return None
unlink(filename)
icon = None
self.iconmap[id] = icon
return icon
def remove(self, id):
self.iconmap.pop(id, None)
unlink(ApplicationData.get(os.path.join('images', id + '.png')))
......@@ -1588,7 +1588,7 @@ class IncomingSession(QObject):
self.dialog.uri_label.setText(address)
if self.contact:
self.dialog.username_label.setText(contact.name or session.remote_identity.display_name or address)
self.dialog.user_icon.setPixmap(contact.icon)
self.dialog.user_icon.setPixmap(contact.pixmap)
else:
self.dialog.username_label.setText(session.remote_identity.display_name or address)
if self.audio_stream:
......@@ -1774,7 +1774,7 @@ class SessionManager(object):
session = Session(account)
if contact is None:
for contact in self.main_window.contact_model.iter_contacts():
if remote_uri.matches(self.normalize_number(account, contact.uri)) or any(remote_uri.matches(self.normalize_number(account, alias)) for alias in contact.sip_aliases):
if any(remote_uri.matches(self.normalize_number(account, uri.uri)) for uri in contact.uris):
break
else:
contact = None
......@@ -1986,18 +1986,16 @@ class SessionManager(object):
session.reject(488)
return
session.send_ring_indication()
uri = session.remote_identity.uri
remote_uri = session.remote_identity.uri
for contact in self.main_window.contact_model.iter_contacts():
if uri.matches(self.normalize_number(session.account, contact.uri)) or any(uri.matches(self.normalize_number(session.account, alias)) for alias in contact.sip_aliases):
if any(remote_uri.matches(self.normalize_number(session.account, uri.uri)) for uri in contact.uris):
break
else:
matched_contacts = []
number = uri.user.strip('0') if self.number_re.match(uri.user) else Null
number = remote_uri.user.strip('0') if self.number_re.match(remote_uri.user) else Null
if len(number) > 7:
for contact in self.main_window.contact_model.iter_contacts():
if self.number_re.match(contact.uri) and self.normalize_number(session.account, contact.uri).endswith(number):
matched_contacts.append(contact)
elif any(self.number_re.match(alias) and self.normalize_number(session.account, alias).endswith(number) for alias in contact.sip_aliases):
if any(self.number_re.match(uri.uri) and self.normalize_number(session.account, uri.uri).endswith(number) for uri in contact.uris):
matched_contacts.append(contact)
contact = matched_contacts[0] if len(matched_contacts)==1 else None
if filetransfer_streams:
......@@ -2033,9 +2031,9 @@ class SessionManager(object):
session.reject_proposal(488)
return
session.send_ring_indication()
uri = session.remote_identity.uri
remote_uri = session.remote_identity.uri
for contact in self.main_window.contact_model.iter_contacts():
if uri.matches(self.normalize_number(session.account, contact.uri)) or any(uri.matches(self.normalize_number(session.account, alias)) for alias in contact.sip_aliases):
if any(remote_uri.matches(self.normalize_number(session.account, uri.uri)) for uri in contact.uris):
break
else:
contact = None
......
......@@ -13,7 +13,7 @@
<property name="palette">
<palette>
<active>
<colorrole role="Base">
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
......@@ -22,18 +22,18 @@
</color>
</brush>
</colorrole>
<colorrole role="Window">
<colorrole role="AlternateBase">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<red>240</red>
<green>244</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="Base">
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
......@@ -42,18 +42,18 @@
</color>
</brush>
</colorrole>
<colorrole role="Window">
<colorrole role="AlternateBase">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<red>240</red>
<green>244</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="Base">
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
......@@ -62,11 +62,11 @@
</color>
</brush>
</colorrole>
<colorrole role="Window">
<colorrole role="AlternateBase">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<red>240</red>
<green>244</green>
<blue>255</blue>
</color>
</brush>
......@@ -94,7 +94,7 @@
<number>0</number>
</property>
<item>
<widget class="QLabel" name="icon">
<widget class="QLabel" name="icon_label">
<property name="minimumSize">
<size>
<width>36</width>
......@@ -113,12 +113,12 @@
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="name_uri_layout">
<layout class="QVBoxLayout" name="name_info_layout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="name">
<widget class="QLabel" name="name_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
......@@ -131,10 +131,7 @@
</widget>
</item>
<item>
<widget class="QLabel" name="uri">
<property name="enabled">
<bool>false</bool>
</property>
<widget class="QLabel" name="info_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
......
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