diff --git a/sipsimple/account/__init__.py b/sipsimple/account/__init__.py index 4a43c855..2e9e1b1f 100644 --- a/sipsimple/account/__init__.py +++ b/sipsimple/account/__init__.py @@ -1,843 +1,842 @@ """ Implements a SIP Account management system that allows the definition of multiple SIP accounts and their properties. """ __all__ = ['Account', 'BonjourAccount', 'AccountManager'] from itertools import chain from threading import Lock from application.notification import IObserver, NotificationCenter, NotificationData from application.python import Null -from application.python.decorator import execute_once from application.python.descriptor import classproperty from application.python.types import Singleton from application.system import host as Host from eventlib import coros, proc from gnutls.crypto import X509Certificate, X509PrivateKey from gnutls.interfaces.twisted import X509Credentials from zope.interface import implementer from sipsimple.account.bonjour import BonjourServices, _bonjour from sipsimple.account.publication import PresencePublisher, DialogPublisher from sipsimple.account.registration import Registrar from sipsimple.account.subscription import MWISubscriber, PresenceWinfoSubscriber, DialogWinfoSubscriber, PresenceSubscriber, SelfPresenceSubscriber, DialogSubscriber from sipsimple.account.xcap import XCAPManager from sipsimple.core import Credentials, SIPURI, ContactURIFactory from sipsimple.configuration import ConfigurationManager, Setting, SettingsGroup, SettingsObject, SettingsObjectID from sipsimple.configuration.datatypes import AudioCodecList, MSRPConnectionModel, MSRPRelayAddress, MSRPTransport, NonNegativeInteger, Path, SIPAddress, SIPProxyAddress, SRTPKeyNegotiation, STUNServerAddressList, VideoCodecList, XCAPRoot from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.payloads import ParserError from sipsimple.payloads.messagesummary import MessageSummary from sipsimple.payloads.pidf import PIDFDocument from sipsimple.payloads.rlsnotify import RLSNotify from sipsimple.payloads.watcherinfo import WatcherInfoDocument from sipsimple.threading import call_in_thread from sipsimple.threading.green import call_in_green_thread, run_in_green_thread -from sipsimple.util import user_info +from sipsimple.util import user_info, execute_once class AuthSettings(SettingsGroup): username = Setting(type=str, default=None, nillable=True) password = Setting(type=str, default='') class SIPSettings(SettingsGroup): always_use_my_proxy = Setting(type=bool, default=False) outbound_proxy = Setting(type=SIPProxyAddress, default=None, nillable=True) register = Setting(type=bool, default=True) register_interval = Setting(type=NonNegativeInteger, default=3600) subscribe_interval = Setting(type=NonNegativeInteger, default=3600) publish_interval = Setting(type=NonNegativeInteger, default=3600) class SRTPEncryptionSettings(SettingsGroup): enabled = Setting(type=bool, default=True) key_negotiation = Setting(type=SRTPKeyNegotiation, default='opportunistic') class RTPSettings(SettingsGroup): audio_codec_list = Setting(type=AudioCodecList, default=None, nillable=True) video_codec_list = Setting(type=VideoCodecList, default=None, nillable=True) encryption = SRTPEncryptionSettings class NATTraversalSettings(SettingsGroup): use_ice = Setting(type=bool, default=False) stun_server_list = Setting(type=STUNServerAddressList, default=None, nillable=True) msrp_relay = Setting(type=MSRPRelayAddress, default=None, nillable=True) use_msrp_relay_for_outbound = Setting(type=bool, default=False) class MessageSummarySettings(SettingsGroup): enabled = Setting(type=bool, default=False) voicemail_uri = Setting(type=SIPAddress, default=None, nillable=True) class XCAPSettings(SettingsGroup): enabled = Setting(type=bool, default=False) discovered = Setting(type=bool, default=False) xcap_root = Setting(type=XCAPRoot, default=None, nillable=True) class PresenceSettings(SettingsGroup): enabled = Setting(type=bool, default=False) class TLSSettings(SettingsGroup): certificate = Setting(type=Path, default=None, nillable=True) verify_server = Setting(type=bool, default=False) class MSRPSettings(SettingsGroup): transport = Setting(type=MSRPTransport, default='tls') connection_model = Setting(type=MSRPConnectionModel, default='relay') @implementer(IObserver) class Account(SettingsObject): """ Object representing a SIP account. Contains configuration settings and attributes for accessing SIP related objects. When the account is active, it will register, publish its presence and subscribe to watcher-info events depending on its settings. If the object is un-pickled and its enabled flag was set, it will automatically activate. When the save method is called, depending on the value of the enabled flag, the account will activate/deactivate. Notifications sent by instances of Account: * CFGSettingsObjectWasCreated * CFGSettingsObjectWasActivated * CFGSettingsObjectWasDeleted * CFGSettingsObjectDidChange * SIPAccountWillActivate * SIPAccountDidActivate * SIPAccountWillDeactivate * SIPAccountDidDeactivate """ __group__ = 'Accounts' __id__ = SettingsObjectID(type=SIPAddress) id = __id__ enabled = Setting(type=bool, default=False) display_name = Setting(type=str, default=None, nillable=True) auth = AuthSettings sip = SIPSettings rtp = RTPSettings nat_traversal = NATTraversalSettings message_summary = MessageSummarySettings msrp = MSRPSettings presence = PresenceSettings xcap = XCAPSettings tls = TLSSettings def __new__(cls, id): - with AccountManager.load.lock: - if not AccountManager.load.called: - raise RuntimeError("cannot instantiate %s before calling AccountManager.load" % cls.__name__) + #with AccountManager.load.lock: + # if not AccountManager.load.called: + # raise RuntimeError("cannot instantiate %s before calling AccountManager.load" % cls.__name__) return SettingsObject.__new__(cls, id) def __init__(self, id): self.contact = ContactURIFactory() self.xcap_manager = XCAPManager(self) self._started = False self._deleted = False self._active = False self._activation_lock = coros.Semaphore(1) self._registrar = Registrar(self) self._mwi_subscriber = MWISubscriber(self) self._pwi_subscriber = PresenceWinfoSubscriber(self) self._dwi_subscriber = DialogWinfoSubscriber(self) self._presence_subscriber = PresenceSubscriber(self) self._self_presence_subscriber = SelfPresenceSubscriber(self) self._dialog_subscriber = DialogSubscriber(self) self._presence_publisher = PresencePublisher(self) self._dialog_publisher = DialogPublisher(self) self._mwi_voicemail_uri = None self._pwi_version = None self._dwi_version = None self._presence_version = None self._dialog_version = None def start(self): if self._started or self._deleted: return self._started = True notification_center = NotificationCenter() notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=self) notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) notification_center.add_observer(self, name='XCAPManagerDidDiscoverServerCapabilities', sender=self.xcap_manager) notification_center.add_observer(self, sender=self._mwi_subscriber) notification_center.add_observer(self, sender=self._pwi_subscriber) notification_center.add_observer(self, sender=self._dwi_subscriber) notification_center.add_observer(self, sender=self._presence_subscriber) notification_center.add_observer(self, sender=self._self_presence_subscriber) notification_center.add_observer(self, sender=self._dialog_subscriber) self.xcap_manager.init() if self.enabled: self._activate() def stop(self): if not self._started: return self._started = False self._deactivate() notification_center = NotificationCenter() notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=self) notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) notification_center.remove_observer(self, name='XCAPManagerDidDiscoverServerCapabilities', sender=self.xcap_manager) notification_center.remove_observer(self, sender=self._mwi_subscriber) notification_center.remove_observer(self, sender=self._pwi_subscriber) notification_center.remove_observer(self, sender=self._dwi_subscriber) notification_center.remove_observer(self, sender=self._presence_subscriber) notification_center.remove_observer(self, sender=self._self_presence_subscriber) notification_center.remove_observer(self, sender=self._dialog_subscriber) def delete(self): if self._deleted: return self._deleted = True self.stop() self._registrar = None self._mwi_subscriber = None self._pwi_subscriber = None self._dwi_subscriber = None self._presence_subscriber = None self._self_presence_subscriber = None self._dialog_subscriber = None self._presence_publisher = None self._dialog_publisher = None self.xcap_manager = None SettingsObject.delete(self) @run_in_green_thread def reregister(self): if self._started: self._registrar.reregister() @run_in_green_thread def resubscribe(self): if self._started: self._mwi_subscriber.resubscribe() self._pwi_subscriber.resubscribe() self._dwi_subscriber.resubscribe() self._presence_subscriber.resubscribe() self._self_presence_subscriber.resubscribe() self._dialog_subscriber.resubscribe() @property def credentials(self): return Credentials(self.auth.username or self.id.username, self.auth.password) @property def registered(self): try: return self._registrar.registered except AttributeError: return False @property def mwi_active(self): try: return self._mwi_subscriber.subscribed except AttributeError: return False @property def tls_credentials(self): # This property can be optimized to cache the credentials it loads from disk, # however this is not a time consuming operation (~ 3000 req/sec). -Luci settings = SIPSimpleSettings() if self.tls.certificate is not None: certificate_data = open(self.tls.certificate.normalized).read() certificate = X509Certificate(certificate_data) private_key = X509PrivateKey(certificate_data) else: certificate = None private_key = None if settings.tls.ca_list is not None: # we should read all certificates in the file, rather than just the first -Luci trusted = [X509Certificate(open(settings.tls.ca_list.normalized).read())] else: trusted = [] credentials = X509Credentials(certificate, private_key, trusted) credentials.verify_peer = self.tls.verify_server return credentials @property def uri(self): return SIPURI(user=self.id.username, host=self.id.domain) @property def voicemail_uri(self): return self._mwi_voicemail_uri or self.message_summary.voicemail_uri @property def presence_state(self): try: return self._presence_publisher.state except AttributeError: return None @presence_state.setter def presence_state(self, state): try: self._presence_publisher.state = state except AttributeError: pass @property def dialog_state(self): try: return self._dialog_publisher.state except AttributeError: return None @dialog_state.setter def dialog_state(self, state): try: self._dialog_publisher.state = state except AttributeError: pass def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) @run_in_green_thread def _NH_CFGSettingsObjectDidChange(self, notification): if self._started and 'enabled' in notification.data.modified: if self.enabled: self._activate() else: self._deactivate() def _NH_XCAPManagerDidDiscoverServerCapabilities(self, notification): if self._started and self.xcap.discovered is False: self.xcap.discovered = True self.save() notification.center.post_notification('SIPAccountDidDiscoverXCAPSupport', sender=self) def _NH_MWISubscriberDidDeactivate(self, notification): self._mwi_voicemail_uri = None def _NH_MWISubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == MessageSummary.content_type: try: message_summary = MessageSummary.parse(notification.data.body) except ParserError: pass else: self._mwi_voicemail_uri = message_summary.message_account and SIPAddress(message_summary.message_account.replace('sip:', '', 1)) or None notification.center.post_notification('SIPAccountGotMessageSummary', sender=self, data=NotificationData(message_summary=message_summary)) def _NH_PresenceWinfoSubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == WatcherInfoDocument.content_type: try: watcher_info = WatcherInfoDocument.parse(notification.data.body) watcher_list = watcher_info['sip:' + self.id] except (ParserError, KeyError): pass else: if watcher_list.package != 'presence': return if self._pwi_version is None: if watcher_info.state == 'partial': self._pwi_subscriber.resubscribe() elif watcher_info.version <= self._pwi_version: return elif watcher_info.state == 'partial' and watcher_info.version > self._pwi_version + 1: self._pwi_subscriber.resubscribe() self._pwi_version = watcher_info.version data = NotificationData(version=watcher_info.version, state=watcher_info.state, watcher_list=watcher_list) notification.center.post_notification('SIPAccountGotPresenceWinfo', sender=self, data=data) def _NH_PresenceWinfoSubscriptionDidEnd(self, notification): self._pwi_version = None def _NH_PresenceWinfoSubscriptionDidFail(self, notification): self._pwi_version = None def _NH_DialogWinfoSubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == WatcherInfoDocument.content_type: try: watcher_info = WatcherInfoDocument.parse(notification.data.body) watcher_list = watcher_info['sip:' + self.id] except (ParserError, KeyError): pass else: if watcher_list.package != 'dialog': return if self._dwi_version is None: if watcher_info.state == 'partial': self._dwi_subscriber.resubscribe() elif watcher_info.version <= self._dwi_version: return elif watcher_info.state == 'partial' and watcher_info.version > self._dwi_version + 1: self._dwi_subscriber.resubscribe() self._dwi_version = watcher_info.version data = NotificationData(version=watcher_info.version, state=watcher_info.state, watcher_list=watcher_list) notification.center.post_notification('SIPAccountGotDialogWinfo', sender=self, data=data) def _NH_DialogWinfoSubscriptionDidEnd(self, notification): self._dwi_version = None def _NH_DialogWinfoSubscriptionDidFail(self, notification): self._dwi_version = None def _NH_PresenceSubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == RLSNotify.content_type: try: rls_notify = RLSNotify.parse('{content_type}\r\n\r\n{body}'.format(content_type=notification.data.headers['Content-Type'], body=notification.data.body)) except ParserError: pass else: if rls_notify.uri != self.xcap_manager.rls_presence_uri: return if self._presence_version is None: if not rls_notify.full_state: self._presence_subscriber.resubscribe() elif rls_notify.version <= self._presence_version: return elif not rls_notify.full_state and rls_notify.version > self._presence_version + 1: self._presence_subscriber.resubscribe() self._presence_version = rls_notify.version data = NotificationData(version=rls_notify.version, full_state=rls_notify.full_state, resource_map=dict((resource.uri, resource) for resource in rls_notify)) notification.center.post_notification('SIPAccountGotPresenceState', sender=self, data=data) def _NH_PresenceSubscriptionDidEnd(self, notification): self._presence_version = None def _NH_PresenceSubscriptionDidFail(self, notification): self._presence_version = None def _NH_SelfPresenceSubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == PIDFDocument.content_type: try: pidf_doc = PIDFDocument.parse(notification.data.body) except ParserError: pass else: if pidf_doc.entity.partition('sip:')[2] != self.id: return notification.center.post_notification('SIPAccountGotSelfPresenceState', sender=self, data=NotificationData(pidf=pidf_doc)) def _NH_DialogSubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == RLSNotify.content_type: try: rls_notify = RLSNotify.parse('{content_type}\r\n\r\n{body}'.format(content_type=notification.data.headers['Content-Type'], body=notification.data.body)) except ParserError: pass else: if rls_notify.uri != self.xcap_manager.rls_dialog_uri: return if self._dialog_version is None: if not rls_notify.full_state: self._dialog_subscriber.resubscribe() elif rls_notify.version <= self._dialog_version: return elif not rls_notify.full_state and rls_notify.version > self._dialog_version + 1: self._dialog_subscriber.resubscribe() self._dialog_version = rls_notify.version data = NotificationData(version=rls_notify.version, full_state=rls_notify.full_state, resource_map=dict((resource.uri, resource) for resource in rls_notify)) notification.center.post_notification('SIPAccountGotDialogState', sender=self, data=data) def _NH_DialogSubscriptionDidEnd(self, notification): self._dialog_version = None def _NH_DialogSubscriptionDidFail(self, notification): self._dialog_version = None def _activate(self): with self._activation_lock: if self._active: return notification_center = NotificationCenter() notification_center.post_notification('SIPAccountWillActivate', sender=self) self._active = True self._registrar.start() self._mwi_subscriber.start() self._pwi_subscriber.start() self._dwi_subscriber.start() self._presence_subscriber.start() self._self_presence_subscriber.start() self._dialog_subscriber.start() self._presence_publisher.start() self._dialog_publisher.start() if self.xcap.enabled: self.xcap_manager.start() notification_center.post_notification('SIPAccountDidActivate', sender=self) def _deactivate(self): with self._activation_lock: if not self._active: return notification_center = NotificationCenter() notification_center.post_notification('SIPAccountWillDeactivate', sender=self) self._active = False handlers = [self._registrar, self._mwi_subscriber, self._pwi_subscriber, self._dwi_subscriber, self._presence_subscriber, self._self_presence_subscriber, self._dialog_subscriber, self._presence_publisher, self._dialog_publisher, self.xcap_manager] proc.waitall([proc.spawn(handler.stop) for handler in handlers]) notification_center.post_notification('SIPAccountDidDeactivate', sender=self) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.id) def __setstate__(self, data): # This restores the password from its previous location as a top level setting # after it was moved under the auth group. SettingsObject.__setstate__(self, data) if not data.get('auth', {}).get('password') and data.get('password'): self.auth.password = data.pop('password') self.save() class BonjourMSRPSettings(SettingsGroup): transport = Setting(type=MSRPTransport, default='tcp') class BonjourAccountEnabledSetting(Setting): def __get__(self, obj, objtype): if obj is None: return self return _bonjour.available and self.values.get(obj, self.default) def __set__(self, obj, value): if not _bonjour.available: raise RuntimeError('mdns support is not available') Setting.__set__(self, obj, value) @implementer(IObserver) class BonjourAccount(SettingsObject): """ Object representing a bonjour account. Contains configuration settings and attributes for accessing bonjour related options. When the account is active, it will send broadcast its contact address on the LAN. If the object is un-pickled and its enabled flag was set, it will automatically activate. When the save method is called, depending on the value of the enabled flag, the account will activate/deactivate. Notifications sent by instances of Account: * CFGSettingsObjectWasCreated * CFGSettingsObjectWasActivated * CFGSettingsObjectWasDeleted * CFGSettingsObjectDidChange * SIPAccountWillActivate * SIPAccountDidActivate * SIPAccountWillDeactivate * SIPAccountDidDeactivate """ __group__ = 'Accounts' __id__ = SIPAddress('bonjour@local') id = property(lambda self: self.__id__) enabled = BonjourAccountEnabledSetting(type=bool, default=True) display_name = Setting(type=str, default=user_info.fullname, nillable=False) msrp = BonjourMSRPSettings presence = PresenceSettings rtp = RTPSettings tls = TLSSettings def __new__(cls): - with AccountManager.load.lock: - if not AccountManager.load.called: - raise RuntimeError("cannot instantiate %s before calling AccountManager.load" % cls.__name__) +# with AccountManager.load.lock: +# if not AccountManager.load.called: +# raise RuntimeError("cannot instantiate %s before calling AccountManager.load" % cls.__name__) return SettingsObject.__new__(cls) def __init__(self): self.contact = ContactURIFactory() self.credentials = None self._started = False self._active = False self._activation_lock = coros.Semaphore(1) self._bonjour_services = BonjourServices(self) # initialize fake settings (these are here to make the bonjour account quack like a duck) self.nat_traversal = NATTraversalSettings() self.nat_traversal.use_ice = False self.nat_traversal.msrp_relay = None self.nat_traversal.use_msrp_relay_for_outbound = False self.xcap = XCAPSettings() self.xcap.enabled = False self.xcap.discovered = False self.xcap.xcap_root = None def __repr__(self): return '%s()' % self.__class__.__name__ def start(self): if self._started: return self._started = True notification_center = NotificationCenter() notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=self) notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) self._bonjour_services.start() if self.enabled: self._activate() def stop(self): if not self._started: return self._started = False self._deactivate() self._bonjour_services.stop() notification_center = NotificationCenter() notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=self) notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) @classproperty def mdns_available(cls): return _bonjour.available @property def registered(self): return False @property def tls_credentials(self): # This property can be optimized to cache the credentials it loads from disk, # however this is not a time consuming operation (~ 3000 req/sec). -Luci settings = SIPSimpleSettings() if self.tls.certificate is not None: certificate_data = open(self.tls.certificate.normalized).read() certificate = X509Certificate(certificate_data) private_key = X509PrivateKey(certificate_data) else: certificate = None private_key = None if settings.tls.ca_list is not None: # we should read all certificates in the file, rather than just the first -Luci trusted = [X509Certificate(open(settings.tls.ca_list.normalized).read())] else: trusted = [] credentials = X509Credentials(certificate, private_key, trusted) credentials.verify_peer = self.tls.verify_server return credentials @property def uri(self): return SIPURI(user=self.contact.username, host=Host.default_ip or '127.0.0.1') @property def presence_state(self): return self._bonjour_services.presence_state @presence_state.setter def presence_state(self, state): self._bonjour_services.presence_state = state def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) @run_in_green_thread def _NH_CFGSettingsObjectDidChange(self, notification): if self._started: if 'enabled' in notification.data.modified: if self.enabled: self._activate() else: self._deactivate() elif self.enabled: if 'display_name' in notification.data.modified: self._bonjour_services.update_registrations() if {'sip.transport_list', 'tls.certificate'}.intersection(notification.data.modified): self._bonjour_services.update_registrations() self._bonjour_services.restart_discovery() def _activate(self): with self._activation_lock: if self._active: return notification_center = NotificationCenter() notification_center.post_notification('SIPAccountWillActivate', sender=self) self._active = True self._bonjour_services.activate() notification_center.post_notification('SIPAccountDidActivate', sender=self) def _deactivate(self): with self._activation_lock: if not self._active: return notification_center = NotificationCenter() notification_center.post_notification('SIPAccountWillDeactivate', sender=self) self._active = False self._bonjour_services.deactivate() notification_center.post_notification('SIPAccountDidDeactivate', sender=self) @implementer(IObserver) class AccountManager(object, metaclass=Singleton): """ This is a singleton object which manages all the SIP accounts. It is also used to manage the default account (the one used for outbound sessions) using the default_account attribute: manager = AccountManager() manager.default_account = manager.get_account('alice@example.net') The following notifications are sent: * SIPAccountManagerDidRemoveAccount * SIPAccountManagerDidAddAccount * SIPAccountManagerDidChangeDefaultAccount """ def __init__(self): self._lock = Lock() self.accounts = {} notification_center = NotificationCenter() notification_center.add_observer(self, name='CFGSettingsObjectWasActivated') notification_center.add_observer(self, name='CFGSettingsObjectWasCreated') @execute_once def load(self): """ Load all accounts from the configuration. The accounts will not be started until the start method is called. """ configuration = ConfigurationManager() bonjour_account = BonjourAccount() names = configuration.get_names([Account.__group__]) [Account(id) for id in names if id != bonjour_account.id] default_account = self.default_account if default_account is None or not default_account.enabled: try: self.default_account = next((account for account in list(self.accounts.values()) if account.enabled)) except StopIteration: self.default_account = None def start(self): """ Start the accounts, which will determine the ones with the enabled flag set to activate. """ notification_center = NotificationCenter() notification_center.post_notification('SIPAccountManagerWillStart', sender=self) proc.waitall([proc.spawn(account.start) for account in list(self.accounts.values())]) notification_center.post_notification('SIPAccountManagerDidStart', sender=self) def stop(self): """ Stop the accounts, which will determine the ones that were enabled to deactivate. This method returns only once the accounts were stopped successfully or they timed out trying. """ notification_center = NotificationCenter() notification_center.post_notification('SIPAccountManagerWillEnd', sender=self) proc.waitall([proc.spawn(account.stop) for account in list(self.accounts.values())]) notification_center.post_notification('SIPAccountManagerDidEnd', sender=self) def has_account(self, id): return id in self.accounts def get_account(self, id): return self.accounts[id] def get_accounts(self): return list(self.accounts.values()) def iter_accounts(self): return iter(list(self.accounts.values())) def find_account(self, contact_uri): # compare contact_address with account contact exact_matches = (account for account in list(self.accounts.values()) if account.enabled and account.contact.username==contact_uri.user) # compare username in contact URI with account username loose_matches = (account for account in list(self.accounts.values()) if account.enabled and account.id.username==contact_uri.user) return next(chain(exact_matches, loose_matches, [None])) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_CFGSettingsObjectWasActivated(self, notification): if isinstance(notification.sender, Account) or (isinstance(notification.sender, BonjourAccount) and _bonjour.available): account = notification.sender self.accounts[account.id] = account notification.center.add_observer(self, sender=account, name='CFGSettingsObjectDidChange') notification.center.add_observer(self, sender=account, name='CFGSettingsObjectWasDeleted') notification.center.post_notification('SIPAccountManagerDidAddAccount', sender=self, data=NotificationData(account=account)) from sipsimple.application import SIPApplication if SIPApplication.running: call_in_green_thread(account.start) def _NH_CFGSettingsObjectWasCreated(self, notification): if isinstance(notification.sender, Account): account = notification.sender if account.enabled and self.default_account is None: self.default_account = account def _NH_CFGSettingsObjectWasDeleted(self, notification): account = notification.sender del self.accounts[account.id] notification.center.remove_observer(self, sender=account, name='CFGSettingsObjectDidChange') notification.center.remove_observer(self, sender=account, name='CFGSettingsObjectWasDeleted') notification.center.post_notification('SIPAccountManagerDidRemoveAccount', sender=self, data=NotificationData(account=account)) def _NH_CFGSettingsObjectDidChange(self, notification): account = notification.sender if '__id__' in notification.data.modified: modified_id = notification.data.modified['__id__'] self.accounts[modified_id.new] = self.accounts.pop(modified_id.old) if 'enabled' in notification.data.modified: if account.enabled and self.default_account is None: self.default_account = account elif not account.enabled and self.default_account is account: try: self.default_account = next((account for account in list(self.accounts.values()) if account.enabled)) except StopIteration: self.default_account = None @property def default_account(self): settings = SIPSimpleSettings() return self.accounts.get(settings.default_account, None) @default_account.setter def default_account(self, account): if account is not None and not account.enabled: raise ValueError("account %s is not enabled" % account.id) notification_center = NotificationCenter() settings = SIPSimpleSettings() with self._lock: old_account = self.accounts.get(settings.default_account, None) if account is old_account: return if account is None: settings.default_account = None else: settings.default_account = account.id settings.save() # we need to post the notification in the file-io thread in order to have it serialized after the # SIPAccountManagerDidAddAccount notification that is triggered when the account is saved the first # time, because save is executed in the file-io thread while this runs in the current thread. -Dan call_in_thread('file-io', notification_center.post_notification, 'SIPAccountManagerDidChangeDefaultAccount', sender=self, data=NotificationData(old_account=old_account, account=account)) diff --git a/sipsimple/account/xcap/__init__.py b/sipsimple/account/xcap/__init__.py index c8105c7f..a424733c 100644 --- a/sipsimple/account/xcap/__init__.py +++ b/sipsimple/account/xcap/__init__.py @@ -1,1765 +1,1764 @@ """High-level management of XCAP documents based on OMA specifications""" __all__ = ['Group', 'Contact', 'ContactURI', 'EventHandling', 'Policy', 'Icon', 'OfflineStatus', 'XCAPManager', 'XCAPTransaction'] import base64 import pickle import os import random import socket import weakref from io import StringIO from collections import OrderedDict from datetime import datetime from itertools import chain from operator import attrgetter from urllib.error import URLError from application.notification import IObserver, NotificationCenter, NotificationData from application.python import Null -from application.python.decorator import execute_once from eventlib import api, coros, proc from eventlib.green.httplib import BadStatusLine from twisted.internet.error import ConnectionLost from xcaplib.green import XCAPClient from xcaplib.error import HTTPError from zope.interface import implementer from sipsimple import log from sipsimple.account.subscription import Subscriber, Content from sipsimple.account.xcap.storage import IXCAPStorage, XCAPStorageError from sipsimple.configuration.datatypes import SIPAddress from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.lookup import DNSLookup, DNSLookupError from sipsimple.payloads import ParserError, IterateTypes, IterateIDs, IterateItems, All from sipsimple.payloads import addressbook, commonpolicy, dialogrules, omapolicy, pidf, prescontent, presrules, resourcelists, rlsservices, xcapcaps, xcapdiff from sipsimple.payloads import rpid; del rpid # needs to be imported to register its namespace from sipsimple.threading import run_in_twisted_thread from sipsimple.threading.green import Command, Worker, run_in_green_thread - +from sipsimple.util import execute_once class XCAPError(Exception): pass class FetchRequiredError(XCAPError): pass class Document(object): name = None application = None payload_type = None default_namespace = None global_tree = None filename = None cached = True def __init__(self, manager): self.manager = weakref.proxy(manager) self.content = None self.etag = None self.fetch_time = datetime.fromtimestamp(0) self.update_time = datetime.fromtimestamp(0) self.dirty = False self.supported = False def __bool__(self): return self.content is not None @property def dirty(self): return self.__dict__['dirty'] or (self.content is not None and self.content.__dirty__) @dirty.setter def dirty(self, dirty): if self.content is not None and not dirty: self.content.__dirty__ = dirty self.__dict__['dirty'] = dirty @property def relative_url(self): return self.url[len(self.manager.xcap_root):].lstrip('/') @property def url(self): return self.manager.client.get_url(self.application, None, globaltree=self.global_tree, filename=self.filename) def load_from_cache(self): if not self.cached: return try: document = StringIO(self.manager.storage.load(self.name)) self.etag = document.readline().strip() or None self.content = self.payload_type.parse(document) self.__dict__['dirty'] = False except (XCAPStorageError, ParserError): self.etag = None self.content = None self.dirty = False self.fetch_time = datetime.utcnow() def initialize(self, server_caps): self.supported = self.application in server_caps.auids if not self.supported: self.reset() def reset(self): if self.cached and self.content is not None: try: self.manager.storage.delete(self.name) except XCAPStorageError: pass self.content = None self.etag = None self.dirty = False def fetch(self): try: document = self.manager.client.get(self.application, etagnot=self.etag, globaltree=self.global_tree, headers={'Accept': self.payload_type.content_type}, filename=self.filename) self.content = self.payload_type.parse(document) self.etag = document.etag self.__dict__['dirty'] = False except (BadStatusLine, ConnectionLost, URLError, socket.error) as e: raise XCAPError("failed to fetch %s document: %s" % (self.name, e)) except HTTPError as e: if e.status == 404: # Not Found if self.content is not None: self.reset() self.fetch_time = datetime.utcnow() elif e.status != 304: # Other than Not Modified: raise XCAPError("failed to fetch %s document: %s" % (self.name, e)) except ParserError as e: raise XCAPError("failed to parse %s document: %s" % (self.name, e)) else: self.fetch_time = datetime.utcnow() if self.cached: try: self.manager.storage.save(self.name, self.etag + os.linesep + document) except XCAPStorageError: pass def update(self): if not self.dirty: return data = self.content.toxml() if self.content is not None else None try: kw = dict(etag=self.etag) if self.etag is not None else dict(etagnot='*') if data is not None: response = self.manager.client.put(self.application, data, globaltree=self.global_tree, filename=self.filename, headers={'Content-Type': self.payload_type.content_type}, **kw) else: response = self.manager.client.delete(self.application, data, globaltree=self.global_tree, filename=self.filename, **kw) except (BadStatusLine, ConnectionLost, URLError) as e: raise XCAPError("failed to update %s document: %s" % (self.name, e)) except HTTPError as e: if e.status == 412: # Precondition Failed raise FetchRequiredError("document %s was modified externally" % self.name) elif e.status == 404 and data is None: # attempted to delete a document that did't exist in the first place pass else: raise XCAPError("failed to update %s document: %s" % (self.name, e)) self.etag = response.etag if data is not None else None self.dirty = False self.update_time = datetime.utcnow() if self.cached: try: if data is not None: self.manager.storage.save(self.name, self.etag + os.linesep + data) else: self.manager.storage.delete(self.name) except XCAPStorageError: pass class DialogRulesDocument(Document): name = 'dialog-rules' application = 'org.openxcap.dialog-rules' payload_type = dialogrules.DialogRulesDocument default_namespace = dialogrules.namespace global_tree = False filename = 'index' class PresRulesDocument(Document): name = 'pres-rules' application = 'org.openmobilealliance.pres-rules' payload_type = presrules.PresRulesDocument default_namespace = presrules.namespace global_tree = False filename = 'index' class ResourceListsDocument(Document): name = 'resource-lists' application = 'resource-lists' payload_type = resourcelists.ResourceListsDocument default_namespace = resourcelists.namespace global_tree = False filename = 'index' def update(self): if self.content is not None: sipsimple_addressbook = self.content['sipsimple_addressbook'] groups = ItemCollection(sipsimple_addressbook[addressbook.Group, IterateItems]) contacts = ItemCollection(sipsimple_addressbook[addressbook.Contact, IterateItems]) policies = ItemCollection(sipsimple_addressbook[addressbook.Policy, IterateItems]) for group, missing_id in ((group, missing_id) for group in groups for missing_id in [id for id in group.contacts if id not in contacts]): group.contacts.remove(missing_id) if any(item.__dirty__ for item in chain(contacts, policies)): oma_grantedcontacts = self.content['oma_grantedcontacts'] oma_blockedcontacts = self.content['oma_blockedcontacts'] dialog_grantedcontacts = self.content['dialog_grantedcontacts'] dialog_blockedcontacts = self.content['dialog_blockedcontacts'] sipsimple_presence_rls = self.content['sipsimple_presence_rls'] sipsimple_dialog_rls = self.content['sipsimple_dialog_rls'] all_contact_uris = set(uri.uri for contact in contacts for uri in contact.uris) contact_allow_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.policy=='allow') contact_block_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.policy=='block') contact_allow_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.policy=='allow') contact_block_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.policy=='block') contact_subscribe_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.subscribe==True) contact_subscribe_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.subscribe==True) policy_allow_presence_uris = set(policy.uri for policy in policies if policy.presence.policy=='allow') policy_block_presence_uris = set(policy.uri for policy in policies if policy.presence.policy=='block') policy_allow_dialog_uris = set(policy.uri for policy in policies if policy.dialog.policy=='allow') policy_block_dialog_uris = set(policy.uri for policy in policies if policy.dialog.policy=='block') policy_subscribe_presence_uris = set(policy.uri for policy in policies if policy.presence.subscribe==True) policy_subscribe_dialog_uris = set(policy.uri for policy in policies if policy.dialog.subscribe==True) allowed_presence_uris = contact_allow_presence_uris - contact_block_presence_uris | policy_allow_presence_uris - policy_block_presence_uris - all_contact_uris blocked_presence_uris = contact_block_presence_uris | policy_block_presence_uris - all_contact_uris allowed_dialog_uris = contact_allow_dialog_uris - contact_block_dialog_uris | policy_allow_dialog_uris - policy_block_dialog_uris - all_contact_uris blocked_dialog_uris = contact_block_dialog_uris | policy_block_dialog_uris - all_contact_uris subscribe_presence_uris = contact_subscribe_presence_uris | policy_subscribe_presence_uris - all_contact_uris subscribe_dialog_uris = contact_subscribe_dialog_uris | policy_subscribe_dialog_uris - all_contact_uris if allowed_presence_uris != set(entry.uri for entry in oma_grantedcontacts): oma_grantedcontacts.clear() oma_grantedcontacts.update(resourcelists.Entry(uri) for uri in allowed_presence_uris) if blocked_presence_uris != set(entry.uri for entry in oma_blockedcontacts): oma_blockedcontacts.clear() oma_blockedcontacts.update(resourcelists.Entry(uri) for uri in blocked_presence_uris) if allowed_dialog_uris != set(entry.uri for entry in dialog_grantedcontacts): dialog_grantedcontacts.clear() dialog_grantedcontacts.update(resourcelists.Entry(uri) for uri in allowed_dialog_uris) if blocked_dialog_uris != set(entry.uri for entry in dialog_blockedcontacts): dialog_blockedcontacts.clear() dialog_blockedcontacts.update(resourcelists.Entry(uri) for uri in blocked_dialog_uris) if subscribe_presence_uris != set(entry.uri for entry in sipsimple_presence_rls): sipsimple_presence_rls.clear() sipsimple_presence_rls.update(resourcelists.Entry(uri) for uri in subscribe_presence_uris) if subscribe_dialog_uris != set(entry.uri for entry in sipsimple_dialog_rls): sipsimple_dialog_rls.clear() sipsimple_dialog_rls.update(resourcelists.Entry(uri) for uri in subscribe_dialog_uris) super(ResourceListsDocument, self).update() class RLSServicesDocument(Document): name = 'rls-services' application = 'rls-services' payload_type = rlsservices.RLSServicesDocument default_namespace = rlsservices.namespace global_tree = False filename = 'index' class XCAPCapsDocument(Document): name = 'xcap-caps' application = 'xcap-caps' payload_type = xcapcaps.XCAPCapabilitiesDocument default_namespace = xcapcaps.namespace global_tree = True filename = 'index' cached = False def initialize(self): self.supported = True class StatusIconDocument(Document): name = 'status-icon' application = 'org.openmobilealliance.pres-content' payload_type = prescontent.PresenceContentDocument default_namespace = prescontent.namespace global_tree = False filename = 'oma_status-icon/index' class PIDFManipulationDocument(Document): name = 'pidf-manipulation' application = 'pidf-manipulation' payload_type = pidf.PIDFDocument default_namespace = pidf.pidf_namespace global_tree = False filename = 'index' class ItemCollection(object): def __init__(self, items): self.items = OrderedDict((item.id, item) for item in items) def __getitem__(self, key): return self.items[key] def __contains__(self, key): return key in self.items def __iter__(self): return iter(list(self.items.values())) def __reversed__(self): return (self[id] for id in reversed(self.items)) def __len__(self): return len(self.items) def __eq__(self, other): if isinstance(other, ItemCollection): return self.items == other.items 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)" % (self.__class__.__name__, list(self.items.values())) def ids(self): return list(self.items.keys()) def iterids(self): return iter(list(self.items.keys())) def get(self, key, default=None): return self.items.get(key, default) def add(self, item): self.items[item.id] = item def remove(self, item): del self.items[item.id] class ContactList(ItemCollection): pass class ContactURIList(ItemCollection): def __init__(self, items, default=None): super(ContactURIList, self).__init__(items) self.default = default def __eq__(self, other): if isinstance(other, ContactURIList): return self.items == other.items and self.default == other.default return NotImplemented def __repr__(self): return "%s(%r, default=%r)" % (self.__class__.__name__, list(self.items.values()), self.default) class Group(object): def __init__(self, id, name, contacts, **attributes): self.id = id self.name = name self.contacts = contacts self.attributes = attributes def __eq__(self, other): if isinstance(other, Group): return self is other or (self.id == other.id and self.name == other.name and self.contacts.ids() == other.contacts.ids() and self.attributes == other.attributes) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __setattr__(self, name, value): if name == 'contacts' and not isinstance(value, ContactList): value = ContactList(value) object.__setattr__(self, name, value) class ContactURI(object): def __init__(self, id, uri, type, **attributes): self.id = id self.uri = uri self.type = type self.attributes = attributes def __eq__(self, other): if isinstance(other, ContactURI): return self is other or (self.id == other.id and self.uri == other.uri and self.type == other.type and self.attributes == other.attributes) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal class EventHandling(object): def __init__(self, policy, subscribe): self.policy = policy self.subscribe = subscribe def __eq__(self, other): if isinstance(other, EventHandling): return self is other or (self.policy == other.policy and self.subscribe == other.subscribe) 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.policy, self.subscribe) class Contact(object): def __init__(self, id, name, uris, presence_handling=None, dialog_handling=None, **attributes): self.id = id self.name = name self.uris = uris self.dialog = dialog_handling or EventHandling(policy='default', subscribe=False) self.presence = presence_handling or EventHandling(policy='default', subscribe=False) self.attributes = attributes def __eq__(self, other): if isinstance(other, Contact): return self is other or (self.id == other.id and self.name == other.name and self.uris == other.uris and self.dialog == other.dialog and self.presence == other.presence and self.attributes == other.attributes) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __setattr__(self, name, value): if name == 'uris' and not isinstance(value, ContactURIList): value = ContactURIList(value) object.__setattr__(self, name, value) class Policy(object): def __init__(self, id, uri, name, presence_handling=None, dialog_handling=None, **attributes): self.id = id self.uri = uri self.name = name self.dialog = dialog_handling or EventHandling(policy='default', subscribe=False) self.presence = presence_handling or EventHandling(policy='default', subscribe=False) self.attributes = attributes def __eq__(self, other): if isinstance(other, Policy): return self is other or (self.id == other.id and self.uri == other.uri and self.name == other.name and self.dialog == other.dialog and self.presence == other.presence and self.attributes == other.attributes) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal class Addressbook(object): def __init__(self, contacts, groups, policies): self.contacts = contacts self.groups = groups self.policies = policies def __eq__(self, other): if isinstance(other, Addressbook): return self is other or (self.contacts == other.contacts and self.groups == other.groups and self.policies == other.policies) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal @classmethod def from_payload(cls, payload): def payload_to_contact(payload): uris = ContactURIList((ContactURI(uri.id, uri.uri, uri.type, **(uri.attributes or {})) for uri in payload.uris), default=payload.uris.default) presence_handling = EventHandling(payload.presence.policy.value, payload.presence.subscribe.value) dialog_handling = EventHandling(payload.dialog.policy.value, payload.dialog.subscribe.value) return Contact(payload.id, payload.name.value, uris, presence_handling, dialog_handling, **(payload.attributes or {})) def payload_to_group(payload): return Group(payload.id, payload.name.value, [contacts[contact_id] for contact_id in payload.contacts], **(payload.attributes or {})) def payload_to_policy(payload): presence_handling = EventHandling(payload.presence.policy.value, payload.presence.subscribe.value) dialog_handling = EventHandling(payload.dialog.policy.value, payload.dialog.subscribe.value) return Policy(payload.id, payload.uri, payload.name.value, presence_handling, dialog_handling, **(payload.attributes or {})) contacts = ItemCollection(payload_to_contact(item) for item in payload[addressbook.Contact, IterateItems]) groups = ItemCollection(payload_to_group(item) for item in payload[addressbook.Group, IterateItems]) policies = ItemCollection(payload_to_policy(item) for item in payload[addressbook.Policy, IterateItems]) return cls(contacts, groups, policies) class PresenceRules(object): def __init__(self, default_policy): self.default_policy = default_policy def __eq__(self, other): if isinstance(other, PresenceRules): return self is other or (self.default_policy == other.default_policy) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal @classmethod def from_payload(cls, default_rule): default_policy = next(item for item in default_rule.actions if isinstance(item, presrules.SubHandling)).value return cls(default_policy) class DialogRules(object): def __init__(self, default_policy): self.default_policy = default_policy def __eq__(self, other): if isinstance(other, DialogRules): return self is other or (self.default_policy == other.default_policy) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal @classmethod def from_payload(cls, default_rule): if default_rule is not None: default_policy = next(item for item in default_rule.actions if isinstance(item, dialogrules.SubHandling)).value else: default_policy = None return cls(default_policy) class Icon(object): __mimetypes__ = ('image/jpeg', 'image/png', 'image/gif') def __init__(self, data, mime_type, description=None): self.data = data self.mime_type = mime_type self.description = description self.url = None self.etag = None def __eq__(self, other): if isinstance(other, Icon): return self is other or (self.data == other.data and self.mime_type == other.mime_type and self.description == other.description) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __setattr__(self, name, value): if name == 'mime_type' and value not in self.__mimetypes__: raise ValueError("invalid mime type: '%s'. Should be one of: %s" % (value, ', '.join(self.__mimetypes__))) object.__setattr__(self, name, value) @classmethod def from_payload(cls, payload): try: data = base64.decodestring(payload.data.value) except Exception: return None else: description = payload.description.value if payload.description else None return cls(data, payload.mime_type.value, description) class OfflineStatus(object): __slots__ = ('pidf',) def __init__(self, pidf): self.pidf = pidf def __setattr__(self, name, value): if name == 'pidf' and not isinstance(value, pidf.PIDF): raise ValueError("pidf must be a PIDF payload") object.__setattr__(self, name, value) def __eq__(self, other): if isinstance(other, OfflineStatus): return self is other or (self.pidf == other.pidf) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __getstate__(self): return {'pidf': self.pidf.toxml()} def __setstate__(self, state): self.pidf = pidf.PIDFDocument.parse(state['pidf']) class Operation(object): __params__ = () def __init__(self, **params): for name, value in list(params.items()): setattr(self, name, value) for param in set(self.__params__).difference(params): raise ValueError("missing operation parameter: '%s'" % param) self.applied = False self.timestamp = datetime.utcnow() class NormalizeOperation(Operation): __params__ = () class AddContactOperation(Operation): __params__ = ('contact',) class UpdateContactOperation(Operation): __params__ = ('contact', 'attributes') class RemoveContactOperation(Operation): __params__ = ('contact',) class AddContactURIOperation(Operation): __params__ = ('contact', 'uri') class UpdateContactURIOperation(Operation): __params__ = ('contact', 'uri', 'attributes') class RemoveContactURIOperation(Operation): __params__ = ('contact', 'uri') class AddGroupOperation(Operation): __params__ = ('group',) class UpdateGroupOperation(Operation): __params__ = ('group', 'attributes') class RemoveGroupOperation(Operation): __params__ = ('group',) class AddGroupMemberOperation(Operation): __params__ = ('group', 'contact') class RemoveGroupMemberOperation(Operation): __params__ = ('group', 'contact') class AddPolicyOperation(Operation): __params__ = ('policy',) class UpdatePolicyOperation(Operation): __params__ = ('policy', 'attributes') class RemovePolicyOperation(Operation): __params__ = ('policy',) class SetDefaultPresencePolicyOperation(Operation): __params__ = ('policy',) class SetDefaultDialogPolicyOperation(Operation): __params__ = ('policy',) class SetStatusIconOperation(Operation): __params__ = ('icon',) class SetOfflineStatusOperation(Operation): __params__ = ('status',) class XCAPSubscriber(Subscriber): __transports__ = frozenset(['tls', 'tcp']) @property def event(self): return 'xcap-diff' @property def content(self): rlist = resourcelists.List() for document in (doc for doc in self.account.xcap_manager.documents if doc.supported): rlist.add(resourcelists.Entry(document.relative_url)) return Content(resourcelists.ResourceLists([rlist]).toxml(), resourcelists.ResourceListsDocument.content_type) @implementer(IObserver) class XCAPManager(object): def __init__(self, account): from sipsimple.application import SIPApplication if SIPApplication.storage is None: raise RuntimeError("SIPApplication.storage must be defined before instantiating XCAPManager") storage = SIPApplication.storage.xcap_storage_factory(account.id) if not IXCAPStorage.providedBy(storage): raise TypeError("storage must implement the IXCAPStorage interface") self.account = account self.storage = storage self.storage_factory = SIPApplication.storage.xcap_storage_factory self.client = None self.command_proc = None self.command_channel = coros.queue() self.last_fetch_time = datetime.fromtimestamp(0) self.last_update_time = datetime.fromtimestamp(0) self.not_executed_fetch = None self.state = 'stopped' self.timer = None self.transaction_level = 0 self.xcap_subscriber = None self.server_caps = XCAPCapsDocument(self) self.dialog_rules = DialogRulesDocument(self) self.pidf_manipulation = PIDFManipulationDocument(self) self.pres_rules = PresRulesDocument(self) self.resource_lists = ResourceListsDocument(self) self.rls_services = RLSServicesDocument(self) self.status_icon = StatusIconDocument(self) for document in self.documents: document.load_from_cache() try: journal = self.storage.load('journal') except XCAPStorageError: self.journal = [] else: try: self.journal = pickle.loads(journal) except Exception: self.journal = [] for operation in self.journal: operation.applied = False notification_center = NotificationCenter() notification_center.add_observer(self, sender=account, name='CFGSettingsObjectDidChange') notification_center.add_observer(self, sender=account, name='CFGSettingsObjectWasDeleted') @property def state(self): return self.__dict__['state'] @state.setter def state(self, value): old_value = self.__dict__.get('state', Null) self.__dict__['state'] = value if old_value != value and old_value is not Null: notification_center = NotificationCenter() notification_center.post_notification('XCAPManagerDidChangeState', sender=self, data=NotificationData(prev_state=old_value, state=value)) @property def documents(self): return [self.resource_lists, self.rls_services, self.pres_rules, self.dialog_rules, self.pidf_manipulation, self.status_icon] @property def document_names(self): return [document.name for document in self.documents] @property def xcap_root(self): return getattr(self.client, 'root', None) @property def rls_presence_uri(self): return SIPAddress('%s+presence@%s' % (self.account.id.username, self.account.id.domain)) @property def rls_dialog_uri(self): return SIPAddress('%s+dialog@%s' % (self.account.id.username, self.account.id.domain)) @execute_once def init(self): """ Initializes the XCAP manager before it can be started. Needs to be called before any other method and in a green thread. """ self.command_proc = proc.spawn(self._run) def start(self): """ Starts the XCAP manager. This method needs to be called in a green thread. """ command = Command('start') self.command_channel.send(command) command.wait() def stop(self): """ Stops the XCAP manager. This method blocks until all the operations are stopped and needs to be called in a green thread. """ command = Command('stop') self.command_channel.send(command) command.wait() def transaction(self): return XCAPTransaction(self) @run_in_twisted_thread def start_transaction(self): self.transaction_level += 1 @run_in_twisted_thread def commit_transaction(self): if self.transaction_level == 0: return self.transaction_level -= 1 if self.transaction_level == 0 and self.journal: self._save_journal() self.command_channel.send(Command('update')) def add_contact(self, contact): self._schedule_operation(AddContactOperation(contact=contact)) def update_contact(self, contact, attributes): self._schedule_operation(UpdateContactOperation(contact=contact, attributes=attributes)) def remove_contact(self, contact): self._schedule_operation(RemoveContactOperation(contact=contact)) def add_contact_uri(self, contact, uri): self._schedule_operation(AddContactURIOperation(contact=contact, uri=uri)) def update_contact_uri(self, contact, uri, attributes): self._schedule_operation(UpdateContactURIOperation(contact=contact, uri=uri, attributes=attributes)) def remove_contact_uri(self, contact, uri): self._schedule_operation(RemoveContactURIOperation(contact=contact, uri=uri)) def add_group(self, group): self._schedule_operation(AddGroupOperation(group=group)) def update_group(self, group, attributes): self._schedule_operation(UpdateGroupOperation(group=group, attributes=attributes)) def remove_group(self, group): self._schedule_operation(RemoveGroupOperation(group=group)) def add_group_member(self, group, contact): self._schedule_operation(AddGroupMemberOperation(group=group, contact=contact)) def remove_group_member(self, group, contact): self._schedule_operation(RemoveGroupMemberOperation(group=group, contact=contact)) def add_policy(self, policy): self._schedule_operation(AddPolicyOperation(policy=policy)) def update_policy(self, policy, attributes): self._schedule_operation(UpdatePolicyOperation(policy=policy, attributes=attributes)) def remove_policy(self, policy): self._schedule_operation(RemovePolicyOperation(policy=policy)) def set_default_presence_policy(self, policy): self._schedule_operation(SetDefaultPresencePolicyOperation(policy=presrules.SubHandlingValue(policy))) def set_default_dialog_policy(self, policy): self._schedule_operation(SetDefaultDialogPolicyOperation(policy=dialogrules.SubHandlingValue(policy))) def set_status_icon(self, icon): self._schedule_operation(SetStatusIconOperation(icon=icon)) def set_offline_status(self, status): self._schedule_operation(SetOfflineStatusOperation(status=status)) @run_in_twisted_thread def _schedule_operation(self, operation): self.journal.append(operation) if self.transaction_level == 0: self._save_journal() self.command_channel.send(Command('update')) def _run(self): while True: command = self.command_channel.wait() try: handler = getattr(self, '_CH_%s' % command.name) handler(command) except: self.command_proc = None raise # Command handlers # def _CH_start(self, command): if self.state != 'stopped': command.signal() return self.state = 'initializing' self.xcap_subscriber = XCAPSubscriber(self.account) notification_center = NotificationCenter() notification_center.post_notification('XCAPManagerWillStart', sender=self) notification_center.add_observer(self, sender=self.xcap_subscriber) notification_center.add_observer(self, sender=SIPSimpleSettings(), name='CFGSettingsObjectDidChange') self.xcap_subscriber.start() self.command_channel.send(Command('initialize')) notification_center.post_notification('XCAPManagerDidStart', sender=self) command.signal() def _CH_stop(self, command): if self.state in ('stopped', 'terminated'): command.signal() return notification_center = NotificationCenter() notification_center.post_notification('XCAPManagerWillEnd', sender=self) notification_center.remove_observer(self, sender=self.xcap_subscriber) notification_center.remove_observer(self, sender=SIPSimpleSettings(), name='CFGSettingsObjectDidChange') if self.timer is not None and self.timer.active(): self.timer.cancel() self.timer = None self.xcap_subscriber.stop() self.xcap_subscriber = None self.client = None self.state = 'stopped' self._save_journal() notification_center.post_notification('XCAPManagerDidEnd', sender=self) command.signal() def _CH_cleanup(self, command): if self.state != 'stopped': command.signal() return try: self.storage.purge() except XCAPStorageError: pass self.journal = [] self.state = 'terminated' command.signal() raise proc.ProcExit def _CH_initialize(self, command): self.state = 'initializing' if self.timer is not None and self.timer.active(): self.timer.cancel() self.timer = None if self.account.xcap.xcap_root: self.client = XCAPClient(self.account.xcap.xcap_root, self.account.id, password=self.account.auth.password) else: try: lookup = DNSLookup() xcap_root = random.choice(lookup.lookup_xcap_server(self.account.uri).wait()) except DNSLookupError: self.timer = self._schedule_command(60, Command('initialize', command.event)) return else: self.client = XCAPClient(xcap_root, self.account.id, password=self.account.auth.password) try: self.server_caps.fetch() except XCAPError: self.timer = self._schedule_command(60, Command('initialize', command.event)) return else: if self.server_caps.content is None: # XCAP server must always return some content for xcap-caps self.timer = self._schedule_command(60, Command('initialize', command.event)) return if not set(self.server_caps.content.auids).issuperset(('resource-lists', 'rls-services', 'org.openmobilealliance.pres-rules')): # Server must support at least resource-lists, rls-services and org.openmobilealliance.pres-rules self.timer = self._schedule_command(3600, Command('initialize', command.event)) return self.server_caps.initialize() for document in self.documents: document.initialize(self.server_caps.content) notification_center = NotificationCenter() notification_center.post_notification('XCAPManagerDidDiscoverServerCapabilities', sender=self, data=NotificationData(auids=self.server_caps.content.auids)) self.state = 'fetching' self.command_channel.send(Command('fetch', documents=set(self.document_names))) self.xcap_subscriber.activate() def _CH_reload(self, command): if self.state == 'terminated': command.signal() return if '__id__' in command.modified: try: self.storage.purge() except XCAPStorageError: pass self.storage = self.storage_factory(self.account.id) self.journal = [] self._save_journal() if {'__id__', 'xcap.xcap_root'}.intersection(command.modified): for document in self.documents: document.reset() if self.state == 'stopped': command.signal() return if {'__id__', 'auth.username', 'auth.password', 'xcap.xcap_root'}.intersection(command.modified): self.state = 'initializing' self.command_channel.send(Command('initialize')) else: self.xcap_subscriber.resubscribe() command.signal() def _CH_fetch(self, command): if self.state not in ('insync', 'fetching'): if self.not_executed_fetch is not None: command.documents.update(self.not_executed_fetch.documents) self.not_executed_fetch = command return if self.not_executed_fetch is not None: command.documents.update(self.not_executed_fetch.documents) self.not_executed_fetch = None self.state = 'fetching' if self.timer is not None and self.timer.active(): command.documents.update(self.timer.command.documents) self.timer.cancel() self.timer = None try: self._fetch_documents(command.documents) except XCAPError: self.timer = self._schedule_command(60, Command('fetch', command.event, documents=command.documents)) return if not self.journal and self.last_fetch_time > datetime.fromtimestamp(0) and all(doc.fetch_time < command.timestamp for doc in self.documents): self.last_fetch_time = datetime.utcnow() self.state = 'insync' return else: self.last_fetch_time = datetime.utcnow() self.state = 'updating' if not self.journal or type(self.journal[0]) is not NormalizeOperation: self.journal.insert(0, NormalizeOperation()) self.command_channel.send(Command('update', command.event)) def _CH_update(self, command): if self.state not in ('insync', 'updating'): return if self.transaction_level != 0: return self.state = 'updating' if self.timer is not None and self.timer.active(): self.timer.cancel() self.timer = None journal = self.journal[:] for operation in (operation for operation in journal if not operation.applied): handler = getattr(self, '_OH_%s' % operation.__class__.__name__) try: handler(operation) except Exception: # Error while applying operation, needs to be logged -Luci log.exception() operation.applied = True api.sleep(0) # Operations are quite CPU intensive try: for document in (doc for doc in self.documents if doc.dirty and doc.supported): document.update() except FetchRequiredError: for document in (doc for doc in self.documents if doc.dirty and doc.supported): document.reset() for operation in journal: operation.applied = False self.state = 'fetching' self.command_channel.send(Command('fetch', documents=set(self.document_names))) # Try to fetch them all just in case except XCAPError: self.timer = self._schedule_command(60, Command('update')) else: del self.journal[:len(journal)] if not self.journal: self.state = 'insync' if any(max(doc.update_time, doc.fetch_time) > self.last_update_time for doc in self.documents): self._load_data() self.last_update_time = datetime.utcnow() command.signal() if self.not_executed_fetch is not None: self.command_channel.send(self.not_executed_fetch) self.not_executed_fetch = None self._save_journal() # Operation handlers # def _OH_NormalizeOperation(self, operation): # Normalize resource-lists # if self.resource_lists.content is None: self.resource_lists.content = resourcelists.ResourceLists() resource_lists = self.resource_lists.content try: oma_buddylist = resource_lists['oma_buddylist'] except KeyError: oma_buddylist = resourcelists.List(name='oma_buddylist') resource_lists.add(oma_buddylist) try: oma_grantedcontacts = resource_lists['oma_grantedcontacts'] except KeyError: oma_grantedcontacts = resourcelists.List(name='oma_grantedcontacts') resource_lists.add(oma_grantedcontacts) try: oma_blockedcontacts = resource_lists['oma_blockedcontacts'] except KeyError: oma_blockedcontacts = resourcelists.List(name='oma_blockedcontacts') resource_lists.add(oma_blockedcontacts) try: oma_allcontacts = resource_lists['oma_allcontacts'] except KeyError: oma_allcontacts = resourcelists.List(name='oma_allcontacts') oma_allcontacts.add(resourcelists.External(self.resource_lists.url + '/~~' + resource_lists.get_xpath(oma_buddylist))) oma_allcontacts.add(resourcelists.External(self.resource_lists.url + '/~~' + resource_lists.get_xpath(oma_grantedcontacts))) oma_allcontacts.add(resourcelists.External(self.resource_lists.url + '/~~' + resource_lists.get_xpath(oma_blockedcontacts))) resource_lists.add(oma_allcontacts) try: dialog_grantedcontacts = resource_lists['dialog_grantedcontacts'] except KeyError: dialog_grantedcontacts = resourcelists.List(name='dialog_grantedcontacts') resource_lists.add(dialog_grantedcontacts) try: dialog_blockedcontacts = resource_lists['dialog_blockedcontacts'] except KeyError: dialog_blockedcontacts = resourcelists.List(name='dialog_blockedcontacts') resource_lists.add(dialog_blockedcontacts) try: sipsimple_presence_rls = resource_lists['sipsimple_presence_rls'] except KeyError: sipsimple_presence_rls = resourcelists.List(name='sipsimple_presence_rls') resource_lists.add(sipsimple_presence_rls) try: sipsimple_dialog_rls = resource_lists['sipsimple_dialog_rls'] except KeyError: sipsimple_dialog_rls = resourcelists.List(name='sipsimple_dialog_rls') resource_lists.add(sipsimple_dialog_rls) try: sipsimple_addressbook = resource_lists['sipsimple_addressbook'] except KeyError: sipsimple_addressbook = resourcelists.List(name='sipsimple_addressbook') resource_lists.add(sipsimple_addressbook) for cls in (cls for cls in sipsimple_addressbook[IterateTypes] if cls not in (addressbook.Contact, addressbook.Group, addressbook.Policy)): del sipsimple_addressbook[cls, All] for cls in (cls for cls in oma_grantedcontacts[IterateTypes] if cls is not resourcelists.Entry): del oma_grantedcontacts[cls, All] for cls in (cls for cls in oma_blockedcontacts[IterateTypes] if cls is not resourcelists.Entry): del oma_blockedcontacts[cls, All] for cls in (cls for cls in dialog_grantedcontacts[IterateTypes] if cls is not resourcelists.Entry): del dialog_grantedcontacts[cls, All] for cls in (cls for cls in dialog_blockedcontacts[IterateTypes] if cls is not resourcelists.Entry): del dialog_blockedcontacts[cls, All] for cls in (cls for cls in sipsimple_presence_rls[IterateTypes] if cls is not resourcelists.Entry): del sipsimple_presence_rls[cls, All] for cls in (cls for cls in sipsimple_dialog_rls[IterateTypes] if cls is not resourcelists.Entry): del sipsimple_dialog_rls[cls, All] groups = ItemCollection(sipsimple_addressbook[addressbook.Group, IterateItems]) contacts = ItemCollection(sipsimple_addressbook[addressbook.Contact, IterateItems]) policies = ItemCollection(sipsimple_addressbook[addressbook.Policy, IterateItems]) for group, missing_id in [(group, missing_id) for group in groups for missing_id in (id for id in group.contacts if id not in contacts)]: group.contacts.remove(missing_id) all_contact_uris = set(uri.uri for contact in contacts for uri in contact.uris) contact_allow_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.policy=='allow') contact_block_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.policy=='block') contact_allow_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.policy=='allow') contact_block_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.policy=='block') contact_subscribe_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.subscribe==True) contact_subscribe_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.subscribe==True) policy_allow_presence_uris = set(policy.uri for policy in policies if policy.presence.policy=='allow') policy_block_presence_uris = set(policy.uri for policy in policies if policy.presence.policy=='block') policy_allow_dialog_uris = set(policy.uri for policy in policies if policy.dialog.policy=='allow') policy_block_dialog_uris = set(policy.uri for policy in policies if policy.dialog.policy=='block') policy_subscribe_presence_uris = set(policy.uri for policy in policies if policy.presence.subscribe==True) policy_subscribe_dialog_uris = set(policy.uri for policy in policies if policy.dialog.subscribe==True) allowed_presence_uris = contact_allow_presence_uris - contact_block_presence_uris | policy_allow_presence_uris - policy_block_presence_uris - all_contact_uris blocked_presence_uris = contact_block_presence_uris | policy_block_presence_uris - all_contact_uris allowed_dialog_uris = contact_allow_dialog_uris - contact_block_dialog_uris | policy_allow_dialog_uris - policy_block_dialog_uris - all_contact_uris blocked_dialog_uris = contact_block_dialog_uris | policy_block_dialog_uris - all_contact_uris subscribe_presence_uris = contact_subscribe_presence_uris | policy_subscribe_presence_uris - all_contact_uris subscribe_dialog_uris = contact_subscribe_dialog_uris | policy_subscribe_dialog_uris - all_contact_uris if allowed_presence_uris != set(entry.uri for entry in oma_grantedcontacts): oma_grantedcontacts.clear() oma_grantedcontacts.update(resourcelists.Entry(uri) for uri in allowed_presence_uris) if blocked_presence_uris != set(entry.uri for entry in oma_blockedcontacts): oma_blockedcontacts.clear() oma_blockedcontacts.update(resourcelists.Entry(uri) for uri in blocked_presence_uris) if allowed_dialog_uris != set(entry.uri for entry in dialog_grantedcontacts): dialog_grantedcontacts.clear() dialog_grantedcontacts.update(resourcelists.Entry(uri) for uri in allowed_dialog_uris) if blocked_dialog_uris != set(entry.uri for entry in dialog_blockedcontacts): dialog_blockedcontacts.clear() dialog_blockedcontacts.update(resourcelists.Entry(uri) for uri in blocked_dialog_uris) if subscribe_presence_uris != set(entry.uri for entry in sipsimple_presence_rls): sipsimple_presence_rls.clear() sipsimple_presence_rls.update(resourcelists.Entry(uri) for uri in subscribe_presence_uris) if subscribe_dialog_uris != set(entry.uri for entry in sipsimple_dialog_rls): sipsimple_dialog_rls.clear() sipsimple_dialog_rls.update(resourcelists.Entry(uri) for uri in subscribe_dialog_uris) # Normalize rls-services # if self.rls_services.content is None: self.rls_services.content = rlsservices.RLSServices() rls_services = self.rls_services.content rls_presence_uri = 'sip:' + self.rls_presence_uri rls_dialog_uri = 'sip:' + self.rls_dialog_uri rls_presence_list = rlsservices.ResourceList(self.resource_lists.url + '/~~' + resource_lists.get_xpath(sipsimple_presence_rls)) rls_dialog_list = rlsservices.ResourceList(self.resource_lists.url + '/~~' + resource_lists.get_xpath(sipsimple_dialog_rls)) try: rls_presence_service = rls_services[rls_presence_uri] except KeyError: rls_presence_service = rlsservices.Service(rls_presence_uri, list=rls_presence_list, packages=['presence']) rls_services.add(rls_presence_service) else: if rls_presence_service.list != rls_presence_list: rls_presence_service.list = rls_presence_list if list(rls_presence_service.packages) != ['presence']: rls_presence_service.packages = ['presence'] try: rls_dialog_service = rls_services[rls_dialog_uri] except KeyError: rls_dialog_service = rlsservices.Service(rls_dialog_uri, list=rls_dialog_list, packages=['dialog']) rls_services.add(rls_dialog_service) else: if rls_dialog_service.list != rls_dialog_list: rls_dialog_service.list = rls_dialog_list if list(rls_dialog_service.packages) != ['dialog']: rls_dialog_service.packages = ['dialog'] # Normalize pres-rules # if self.pres_rules.content is None: self.pres_rules.content = presrules.PresRules() def fix_subhandling(rule, valid_values=[]): subhandling_elements = sorted((item for item in rule.actions if isinstance(item, presrules.SubHandling)), key=attrgetter('value.priority')) if not subhandling_elements: subhandling_elements = [presrules.SubHandling('block')] # spec specifies that missing SubHandling means block rule.actions.update(subhandling_elements) subhandling = subhandling_elements.pop() for item in subhandling_elements: # remove any extraneous SubHandling elements rule.actions.remove(item) if subhandling.value not in valid_values: subhandling.value = valid_values[0] pres_rules = self.pres_rules.content oma_grantedcontacts_ref = omapolicy.ExternalList([self.resource_lists.url + '/~~' + resource_lists.get_xpath(oma_grantedcontacts)]) oma_blockedcontacts_ref = omapolicy.ExternalList([self.resource_lists.url + '/~~' + resource_lists.get_xpath(oma_blockedcontacts)]) try: wp_prs_grantedcontacts = pres_rules['wp_prs_grantedcontacts'] except KeyError: wp_prs_grantedcontacts = commonpolicy.Rule('wp_prs_grantedcontacts', conditions=[oma_grantedcontacts_ref], actions=[presrules.SubHandling('allow')]) pres_rules.add(wp_prs_grantedcontacts) else: fix_subhandling(wp_prs_grantedcontacts, valid_values=['allow']) if list(wp_prs_grantedcontacts.conditions) != [oma_grantedcontacts_ref]: wp_prs_grantedcontacts.conditions = [oma_grantedcontacts_ref] if wp_prs_grantedcontacts.transformations: wp_prs_grantedcontacts.transformations = None try: wp_prs_blockedcontacts = pres_rules['wp_prs_blockedcontacts'] except KeyError: wp_prs_blockedcontacts = commonpolicy.Rule('wp_prs_blockedcontacts', conditions=[oma_blockedcontacts_ref], actions=[presrules.SubHandling('polite-block')]) pres_rules.add(wp_prs_blockedcontacts) else: fix_subhandling(wp_prs_blockedcontacts, valid_values=['polite-block']) if list(wp_prs_blockedcontacts.conditions) != [oma_blockedcontacts_ref]: wp_prs_blockedcontacts.conditions = [oma_blockedcontacts_ref] if wp_prs_blockedcontacts.transformations: wp_prs_blockedcontacts.transformations = None wp_prs_unlisted = pres_rules.get('wp_prs_unlisted', None) wp_prs_allow_unlisted = pres_rules.get('wp_prs_allow_unlisted', None) if wp_prs_unlisted is not None and wp_prs_allow_unlisted is not None: pres_rules.remove(wp_prs_allow_unlisted) wp_prs_allow_unlisted = None wp_prs_unlisted_rule = wp_prs_unlisted or wp_prs_allow_unlisted if wp_prs_unlisted_rule is None: wp_prs_unlisted = commonpolicy.Rule('wp_prs_unlisted', conditions=[omapolicy.OtherIdentity()], actions=[presrules.SubHandling('confirm')]) pres_rules.add(wp_prs_unlisted) wp_prs_unlisted_rule = wp_prs_unlisted else: if wp_prs_unlisted_rule is wp_prs_unlisted: fix_subhandling(wp_prs_unlisted_rule, valid_values=['confirm', 'block', 'polite-block']) else: fix_subhandling(wp_prs_unlisted_rule, valid_values=['allow']) if list(wp_prs_unlisted_rule.conditions) != [omapolicy.OtherIdentity()]: wp_prs_unlisted_rule.conditions = [omapolicy.OtherIdentity()] if wp_prs_unlisted_rule.transformations: wp_prs_unlisted_rule.transformations = None match_anonymous = omapolicy.AnonymousRequest() try: wp_prs_block_anonymous = pres_rules['wp_prs_block_anonymous'] except KeyError: wp_prs_block_anonymous = commonpolicy.Rule('wp_prs_block_anonymous', conditions=[match_anonymous], actions=[presrules.SubHandling('block')]) pres_rules.add(wp_prs_block_anonymous) else: fix_subhandling(wp_prs_block_anonymous, valid_values=['block', 'polite-block']) if list(wp_prs_block_anonymous.conditions) != [match_anonymous]: wp_prs_block_anonymous.conditions = [match_anonymous] if wp_prs_block_anonymous.transformations: wp_prs_block_anonymous.transformations = None match_self = commonpolicy.Identity([commonpolicy.IdentityOne('sip:' + self.account.id)]) try: wp_prs_allow_own = pres_rules['wp_prs_allow_own'] except KeyError: wp_prs_allow_own = commonpolicy.Rule('wp_prs_allow_own', conditions=[match_self], actions=[presrules.SubHandling('allow')]) pres_rules.add(wp_prs_allow_own) else: fix_subhandling(wp_prs_allow_own, valid_values=['allow']) if list(wp_prs_allow_own.conditions) != [match_self]: wp_prs_allow_own.conditions = [match_self] if wp_prs_allow_own.transformations: wp_prs_allow_own.transformations = None # Remove any other rules all_rule_names = set(pres_rules[IterateIDs]) known_rule_names = {'wp_prs_grantedcontacts', 'wp_prs_blockedcontacts', 'wp_prs_unlisted', 'wp_prs_allow_unlisted', 'wp_prs_block_anonymous', 'wp_prs_allow_own'} for name in all_rule_names - known_rule_names: del pres_rules[name] del fix_subhandling # Normalize dialog-rules # if self.dialog_rules.supported: if self.dialog_rules.content is None: self.dialog_rules.content = dialogrules.DialogRules() elif self.dialog_rules.content.element.nsmap.get('dr') != dialogrules.namespace: # TODO: this elif branch should be removed in a later version as it is self.dialog_rules.content = dialogrules.DialogRules() # only used to discard documents created with the old namespace. -Dan def fix_subhandling(rule, valid_values=()): subhandling_elements = sorted((item for item in rule.actions if isinstance(item, dialogrules.SubHandling)), key=attrgetter('value.priority')) if not subhandling_elements: subhandling_elements = [dialogrules.SubHandling('block')] # spec specifies that missing SubHandling means block rule.actions.update(subhandling_elements) subhandling = subhandling_elements.pop() for item in subhandling_elements: # remove any extraneous SubHandling elements rule.actions.remove(item) if subhandling.value not in valid_values: subhandling.value = valid_values[0] dialog_rules = self.dialog_rules.content dialog_grantedcontacts_ref = omapolicy.ExternalList([self.resource_lists.url + '/~~' + resource_lists.get_xpath(dialog_grantedcontacts)]) dialog_blockedcontacts_ref = omapolicy.ExternalList([self.resource_lists.url + '/~~' + resource_lists.get_xpath(dialog_blockedcontacts)]) try: wp_dlg_grantedcontacts = dialog_rules['wp_dlg_grantedcontacts'] except KeyError: wp_dlg_grantedcontacts = commonpolicy.Rule('wp_dlg_grantedcontacts', conditions=[dialog_grantedcontacts_ref], actions=[dialogrules.SubHandling('allow')]) dialog_rules.add(wp_dlg_grantedcontacts) else: fix_subhandling(wp_dlg_grantedcontacts, valid_values=['allow']) if list(wp_dlg_grantedcontacts.conditions) != [dialog_grantedcontacts_ref]: wp_dlg_grantedcontacts.conditions = [dialog_grantedcontacts_ref] if wp_dlg_grantedcontacts.transformations: wp_dlg_grantedcontacts.transformations = None try: wp_dlg_blockedcontacts = dialog_rules['wp_dlg_blockedcontacts'] except KeyError: wp_dlg_blockedcontacts = commonpolicy.Rule('wp_dlg_blockedcontacts', conditions=[dialog_blockedcontacts_ref], actions=[dialogrules.SubHandling('polite-block')]) dialog_rules.add(wp_dlg_blockedcontacts) else: fix_subhandling(wp_dlg_blockedcontacts, valid_values=['polite-block']) if list(wp_dlg_blockedcontacts.conditions) != [dialog_blockedcontacts_ref]: wp_dlg_blockedcontacts.conditions = [dialog_blockedcontacts_ref] if wp_dlg_blockedcontacts.transformations: wp_dlg_blockedcontacts.transformations = None wp_dlg_unlisted = dialog_rules.get('wp_dlg_unlisted', None) wp_dlg_allow_unlisted = dialog_rules.get('wp_dlg_allow_unlisted', None) if wp_dlg_unlisted is not None and wp_dlg_allow_unlisted is not None: dialog_rules.remove(wp_dlg_allow_unlisted) wp_dlg_allow_unlisted = None wp_dlg_unlisted_rule = wp_dlg_unlisted or wp_dlg_allow_unlisted if wp_dlg_unlisted_rule is None: wp_dlg_unlisted = commonpolicy.Rule('wp_dlg_unlisted', conditions=[omapolicy.OtherIdentity()], actions=[dialogrules.SubHandling('confirm')]) dialog_rules.add(wp_dlg_unlisted) wp_dlg_unlisted_rule = wp_dlg_unlisted else: if wp_dlg_unlisted_rule is wp_dlg_unlisted: fix_subhandling(wp_dlg_unlisted_rule, valid_values=['confirm', 'block', 'polite-block']) else: fix_subhandling(wp_dlg_unlisted_rule, valid_values=['allow']) if list(wp_dlg_unlisted_rule.conditions) != [omapolicy.OtherIdentity()]: wp_dlg_unlisted_rule.conditions = [omapolicy.OtherIdentity()] if wp_dlg_unlisted_rule.transformations: wp_dlg_unlisted_rule.transformations = None match_anonymous = omapolicy.AnonymousRequest() try: wp_dlg_block_anonymous = dialog_rules['wp_dlg_block_anonymous'] except KeyError: wp_dlg_block_anonymous = commonpolicy.Rule('wp_dlg_block_anonymous', conditions=[match_anonymous], actions=[dialogrules.SubHandling('block')]) dialog_rules.add(wp_dlg_block_anonymous) else: fix_subhandling(wp_dlg_block_anonymous, valid_values=['block', 'polite-block']) if list(wp_dlg_block_anonymous.conditions) != [match_anonymous]: wp_dlg_block_anonymous.conditions = [match_anonymous] if wp_dlg_block_anonymous.transformations: wp_dlg_block_anonymous.transformations = None match_self = commonpolicy.Identity([commonpolicy.IdentityOne('sip:' + self.account.id)]) try: wp_dlg_allow_own = dialog_rules['wp_dlg_allow_own'] except KeyError: wp_dlg_allow_own = commonpolicy.Rule('wp_dlg_allow_own', conditions=[match_self], actions=[dialogrules.SubHandling('allow')]) dialog_rules.add(wp_dlg_allow_own) else: fix_subhandling(wp_dlg_allow_own, valid_values=['allow']) if list(wp_dlg_allow_own.conditions) != [match_self]: wp_dlg_allow_own.conditions = [match_self] if wp_dlg_allow_own.transformations: wp_dlg_allow_own.transformations = None # Remove any other rules all_rule_names = set(dialog_rules[IterateIDs]) known_rule_names = {'wp_dlg_grantedcontacts', 'wp_dlg_blockedcontacts', 'wp_dlg_unlisted', 'wp_dlg_allow_unlisted', 'wp_dlg_block_anonymous', 'wp_dlg_allow_own'} for name in all_rule_names - known_rule_names: del dialog_rules[name] # Normalize status icon # if self.status_icon.supported and self.status_icon.content is not None: content = self.status_icon.content if None in (content.encoding, content.mime_type) or content.encoding.value.lower() != 'base64' or content.mime_type.value.lower() not in Icon.__mimetypes__: self.status_icon.content = None self.status_icon.dirty = True def _OH_AddContactOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] contact = operation.contact presence_handling = addressbook.PresenceHandling(contact.presence.policy, contact.presence.subscribe) dialog_handling = addressbook.DialogHandling(contact.dialog.policy, contact.dialog.subscribe) xml_contact = addressbook.Contact(contact.id, contact.name, presence_handling=presence_handling, dialog_handling=dialog_handling) for uri in contact.uris: contact_uri = addressbook.ContactURI(uri.id, uri.uri, uri.type) contact_uri.attributes = addressbook.ContactURI.attributes.type(uri.attributes) xml_contact.uris.add(contact_uri) xml_contact.uris.default = contact.uris.default xml_contact.attributes = addressbook.Contact.attributes.type(contact.attributes) sipsimple_addressbook.add(xml_contact) def _OH_UpdateContactOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: contact = sipsimple_addressbook[addressbook.Contact, operation.contact.id] except KeyError: return attributes = dict(operation.attributes) attributes.pop('id', None) # id is never modified attributes.pop('uris', None) # uris are modified using dedicated methods if 'name' in attributes: contact.name = attributes.pop('name') if 'uris.default' in attributes: contact.uris.default = attributes.pop('uris.default') if 'presence.policy' in attributes: contact.presence.policy = attributes.pop('presence.policy') if 'presence.subscribe' in attributes: contact.presence.subscribe = attributes.pop('presence.subscribe') if 'dialog.policy' in attributes: contact.dialog.policy = attributes.pop('dialog.policy') if 'dialog.subscribe' in attributes: contact.dialog.subscribe = attributes.pop('dialog.subscribe') if contact.attributes is None: contact.attributes = addressbook.Contact.attributes.type() contact.attributes.update(attributes) def _OH_RemoveContactOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] for group in (group for group in sipsimple_addressbook[addressbook.Group, IterateItems] if operation.contact.id in group.contacts): group.contacts.remove(operation.contact.id) try: del sipsimple_addressbook[addressbook.Contact, operation.contact.id] except KeyError: pass def _OH_AddContactURIOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: contact = sipsimple_addressbook[addressbook.Contact, operation.contact.id] except KeyError: return uri = addressbook.ContactURI(operation.uri.id, operation.uri.uri, operation.uri.type) uri.attributes = addressbook.ContactURI.attributes.type(operation.uri.attributes) contact.uris.add(uri) def _OH_UpdateContactURIOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: contact = sipsimple_addressbook[addressbook.Contact, operation.contact.id] uri = contact.uris[operation.uri.id] except KeyError: return attributes = dict(operation.attributes) attributes.pop('id', None) # id is never modified if 'uri' in attributes: uri.uri = attributes.pop('uri') if 'type' in attributes: uri.type = attributes.pop('type') if uri.attributes is None: uri.attributes = addressbook.ContactURI.attributes.type() uri.attributes.update(attributes) def _OH_RemoveContactURIOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: contact = sipsimple_addressbook[addressbook.Contact, operation.contact.id] del contact.uris[operation.uri.id] except KeyError: pass def _OH_AddGroupOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] group = addressbook.Group(operation.group.id, operation.group.name, [contact.id for contact in operation.group.contacts]) group.attributes = addressbook.Group.attributes.type(operation.group.attributes) sipsimple_addressbook.add(group) def _OH_UpdateGroupOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: group = sipsimple_addressbook[addressbook.Group, operation.group.id] except KeyError: return attributes = dict(operation.attributes) attributes.pop('id', None) # id is never modified attributes.pop('contacts', None) # contacts are added/removed using dedicated methods if 'name' in attributes: group.name = attributes.pop('name') if group.attributes is None: group.attributes = addressbook.Group.attributes.type() group.attributes.update(attributes) def _OH_RemoveGroupOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: del sipsimple_addressbook[addressbook.Group, operation.group.id] except KeyError: pass def _OH_AddGroupMemberOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: group = sipsimple_addressbook[addressbook.Group, operation.group.id] except KeyError: return if operation.contact.id in group.contacts: return group.contacts.add(operation.contact.id) def _OH_RemoveGroupMemberOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: group = sipsimple_addressbook[addressbook.Group, operation.group.id] group.contacts.remove(operation.contact.id) except KeyError: return def _OH_AddPolicyOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] presence_handling = addressbook.PresenceHandling(operation.policy.presence.policy, operation.policy.presence.subscribe) dialog_handling = addressbook.DialogHandling(operation.policy.dialog.policy, operation.policy.dialog.subscribe) policy = addressbook.Policy(operation.policy.id, operation.policy.uri, operation.policy.name, presence_handling=presence_handling, dialog_handling=dialog_handling) policy.attributes = addressbook.Policy.attributes.type(operation.policy.attributes) sipsimple_addressbook.add(policy) def _OH_UpdatePolicyOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: policy = sipsimple_addressbook[addressbook.Policy, operation.policy.id] except KeyError: return attributes = dict(operation.attributes) attributes.pop('id', None) # id is never modified if 'uri' in attributes: policy.uri = attributes.pop('uri') if 'name' in attributes: policy.name = attributes.pop('name') if 'presence.policy' in attributes: policy.presence.policy = attributes.pop('presence.policy') if 'presence.subscribe' in attributes: policy.presence.subscribe = attributes.pop('presence.subscribe') if 'dialog.policy' in attributes: policy.dialog.policy = attributes.pop('dialog.policy') if 'dialog.subscribe' in attributes: policy.dialog.subscribe = attributes.pop('dialog.subscribe') if policy.attributes is None: policy.attributes = addressbook.Policy.attributes.type() policy.attributes.update(attributes) def _OH_RemovePolicyOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: del sipsimple_addressbook[addressbook.Policy, operation.policy.id] except KeyError: pass def _OH_SetStatusIconOperation(self, operation): if not self.status_icon.supported: return icon = operation.icon if icon is None or not icon.data: self.status_icon.dirty = self.status_icon.content is not None self.status_icon.content = None else: content = prescontent.PresenceContent(data=base64.encodestring(icon.data), mime_type=icon.mime_type, encoding='base64', description=icon.description) if self.status_icon.content == content: return self.status_icon.content = content def _OH_SetOfflineStatusOperation(self, operation): pidf = operation.status.pidf if operation.status is not None else None if not self.pidf_manipulation.supported or pidf == self.pidf_manipulation.content: return self.pidf_manipulation.content = pidf self.pidf_manipulation.dirty = True def _OH_SetDefaultPresencePolicyOperation(self, operation): pres_rules = self.pres_rules.content if operation.policy == 'allow': rule_id, other_rule_id = 'wp_prs_allow_unlisted', 'wp_prs_unlisted' else: rule_id, other_rule_id = 'wp_prs_unlisted', 'wp_prs_allow_unlisted' try: del pres_rules[other_rule_id] except KeyError: rule = pres_rules[rule_id] subhandling = next(item for item in rule.actions if isinstance(item, presrules.SubHandling)) subhandling.value = operation.policy else: rule = commonpolicy.Rule(rule_id, conditions=[omapolicy.OtherIdentity()], actions=[presrules.SubHandling(operation.policy)]) pres_rules.add(rule) def _OH_SetDefaultDialogPolicyOperation(self, operation): if not self.dialog_rules.supported: return dialog_rules = self.dialog_rules.content if operation.policy == 'allow': rule_id, other_rule_id = 'wp_dlg_allow_unlisted', 'wp_dlg_unlisted' else: rule_id, other_rule_id = 'wp_dlg_unlisted', 'wp_dlg_allow_unlisted' try: del dialog_rules[other_rule_id] except KeyError: rule = dialog_rules[rule_id] subhandling = next(item for item in rule.actions if isinstance(item, dialogrules.SubHandling)) subhandling.value = operation.policy else: rule = commonpolicy.Rule(rule_id, conditions=[omapolicy.OtherIdentity()], actions=[dialogrules.SubHandling(operation.policy)]) dialog_rules.add(rule) # Notification handlers # @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) @run_in_green_thread def _NH_CFGSettingsObjectDidChange(self, notification): if {'__id__', 'xcap.xcap_root', 'auth.username', 'auth.password', 'sip.subscribe_interval', 'sip.transport_list'}.intersection(notification.data.modified): self.command_channel.send(Command('reload', modified=notification.data.modified)) if 'enabled' in notification.data.modified: return # global account activation is handled separately by the account itself if self.account.enabled and 'xcap.enabled' in notification.data.modified: if self.account.xcap.enabled: self.start() else: self.stop() def _NH_CFGSettingsObjectWasDeleted(self, notification): notification.center.remove_observer(self, sender=self.account, name='CFGSettingsObjectDidChange') notification.center.remove_observer(self, sender=self.account, name='CFGSettingsObjectWasDeleted') self.command_channel.send(Command('stop')) self.command_channel.send(Command('cleanup')) def _NH_XCAPSubscriptionDidStart(self, notification): self.command_channel.send(Command('fetch', documents=set(self.document_names))) def _NH_XCAPSubscriptionDidFail(self, notification): self.command_channel.send(Command('fetch', documents=set(self.document_names))) def _NH_XCAPSubscriptionGotNotify(self, notification): if notification.data.content_type == xcapdiff.XCAPDiffDocument.content_type: try: xcap_diff = xcapdiff.XCAPDiffDocument.parse(notification.data.body) except ParserError: self.command_channel.send(Command('fetch', documents=set(self.document_names))) else: applications = set(child.selector.auid for child in xcap_diff if isinstance(child, xcapdiff.Document)) documents = set(document.name for document in self.documents if document.application in applications) self.command_channel.send(Command('fetch', documents=documents)) def _load_data(self): addressbook = Addressbook.from_payload(self.resource_lists.content['sipsimple_addressbook']) default_presence_rule = self.pres_rules.content.get('wp_prs_unlisted', None) or self.pres_rules.content.get('wp_prs_allow_unlisted', None) if self.dialog_rules.supported: default_dialog_rule = self.dialog_rules.content.get('wp_dlg_unlisted', None) or self.dialog_rules.content.get('wp_dlg_allow_unlisted', None) else: default_dialog_rule = None presence_rules = PresenceRules.from_payload(default_presence_rule) dialog_rules = DialogRules.from_payload(default_dialog_rule) if self.status_icon.supported and self.status_icon.content: status_icon = Icon.from_payload(self.status_icon.content) status_icon.url = self.status_icon.url status_icon.etag = self.status_icon.etag else: status_icon = None if self.pidf_manipulation.supported and self.pidf_manipulation.content: offline_status = OfflineStatus(self.pidf_manipulation.content) else: offline_status = None data=NotificationData(addressbook=addressbook, presence_rules=presence_rules, dialog_rules=dialog_rules, status_icon=status_icon, offline_status=offline_status) NotificationCenter().post_notification('XCAPManagerDidReloadData', sender=self, data=data) def _fetch_documents(self, documents): workers = [Worker.spawn(document.fetch) for document in (doc for doc in self.documents if doc.name in documents and doc.supported)] try: while workers: worker = workers.pop() worker.wait() finally: for worker in workers: worker.wait_ex() def _save_journal(self): try: self.storage.save('journal', pickle.dumps(self.journal)) except XCAPStorageError: pass def _schedule_command(self, timeout, command): from twisted.internet import reactor timer = reactor.callLater(timeout, self.command_channel.send, command) timer.command = command return timer class XCAPTransaction(object): def __init__(self, xcap_manager): self.xcap_manager = xcap_manager def __enter__(self): self.xcap_manager.start_transaction() return self def __exit__(self, type, value, traceback): self.xcap_manager.commit_transaction() diff --git a/sipsimple/addressbook.py b/sipsimple/addressbook.py index 7fc54812..e054df7f 100644 --- a/sipsimple/addressbook.py +++ b/sipsimple/addressbook.py @@ -1,1365 +1,1365 @@ """Implementation of an addressbook management system""" __all__ = ['AddressbookManager', 'Contact', 'ContactURI', 'Group', 'Policy', 'SharedSetting', 'ContactExtension', 'ContactURIExtension', 'GroupExtension', 'PolicyExtension'] from functools import reduce from operator import attrgetter from random import randint from .threading import Lock from time import time from zope.interface import implementer from application.notification import IObserver, NotificationCenter, NotificationData from application.python import Null -from application.python.decorator import execute_once from application.python.types import Singleton, MarkerType from application.python.weakref import defaultweakobjectmap from sipsimple import log from sipsimple.account import xcap, AccountManager from sipsimple.configuration import ConfigurationManager, ObjectNotFoundError, DuplicateIDError, PersistentKey, ModifiedValue, ModifiedList from sipsimple.configuration import AbstractSetting, RuntimeSetting, SettingsObjectImmutableID, SettingsGroup, SettingsGroupMeta, SettingsState, ItemCollection, ItemManagement from sipsimple.payloads.addressbook import PolicyValue, ElementAttributes from sipsimple.payloads.datatypes import ID from sipsimple.payloads.resourcelists import ResourceListsDocument from sipsimple.threading import run_in_thread +from sipsimple.util import execute_once def unique_id(prefix='id'): return "%s%d%06d" % (prefix, time()*1e6, randint(0, 999999)) def recursive_getattr(obj, name): return reduce(getattr, name.split('.'), obj) class Local(object, metaclass=MarkerType): pass class Remote(object): def __init__(self, account, xcap_object): self.account = account self.xcap_object = xcap_object def __repr__(self): return "%s(%r, %r)" % (self.__class__.__name__, self.account, self.xcap_object) class Setting(AbstractSetting): """ Descriptor representing a setting in an addressbook object. Unlike a standard Setting, this one will only use the default value as a template to fill in a missing value and explicitly set it when saving if it was not specified explicitly prior to that. """ def __init__(self, type, default=None, nillable=False): if default is None and not nillable: raise TypeError("default must be specified if object is not nillable") self.type = type self.default = default self.nillable = nillable self.values = defaultweakobjectmap(lambda: default) self.oldvalues = defaultweakobjectmap(lambda: default) self.dirty = defaultweakobjectmap(bool) self.lock = Lock() def __get__(self, obj, objtype): if obj is None: return self with self.lock: return self.values[obj] def __set__(self, obj, value): if value is None and not self.nillable: raise ValueError("setting attribute is not nillable") if value is not None and not isinstance(value, self.type): value = self.type(value) with self.lock: self.values[obj] = value self.dirty[obj] = value != self.oldvalues[obj] def __getstate__(self, obj): with self.lock: value = self.values[obj] if value is None: pass elif issubclass(self.type, bool): value = 'true' if value else 'false' elif issubclass(self.type, (int, int, str)): value = str(value) elif hasattr(value, '__getstate__'): value = value.__getstate__() else: value = str(value) return value def __setstate__(self, obj, value): if value is None and not self.nillable: raise ValueError("setting attribute is not nillable") if value is None: pass elif issubclass(self.type, bool): if value.lower() in ('true', 'yes', 'on', '1'): value = True elif value.lower() in ('false', 'no', 'off', '0'): value = False else: raise ValueError("invalid boolean value: %s" % (value,)) elif issubclass(self.type, (int, int, str)): value = self.type(value) elif hasattr(self.type, '__setstate__'): object = self.type.__new__(self.type) object.__setstate__(value) value = object else: value = self.type(value) with self.lock: self.oldvalues[obj] = self.values[obj] = value self.dirty[obj] = False def get_modified(self, obj): with self.lock: try: if self.dirty[obj]: return ModifiedValue(old=self.oldvalues[obj], new=self.values[obj]) else: return None finally: self.oldvalues[obj] = self.values[obj] self.dirty[obj] = False def get_old(self, obj): with self.lock: return self.oldvalues[obj] def undo(self, obj): with self.lock: self.values[obj] = self.oldvalues[obj] self.dirty[obj] = False class SharedSetting(Setting): """A setting that is shared by being also stored remotely in XCAP""" __namespace__ = None @classmethod def set_namespace(cls, namespace): """ Set the XML namespace to be used for the extra shared attributes of a contact, when storing it in XCAP """ if cls.__namespace__ is not None: raise RuntimeError("namespace already set to %s" % cls.__namespace__) cls.__namespace__ = namespace class ApplicationElementAttributes(ElementAttributes): _xml_namespace = 'urn:%s:xml:ns:addressbook' % namespace ResourceListsDocument.unregister_namespace(ElementAttributes._xml_namespace) ResourceListsDocument.register_namespace(ApplicationElementAttributes._xml_namespace, prefix=namespace.rpartition(':')[2]) for cls, attribute_name in ((cls, name) for cls in list(ResourceListsDocument.element_map.values()) for name, elem in list(cls._xml_element_children.items()) if elem.type is ElementAttributes): cls.unregister_extension(attribute_name) cls.register_extension(attribute_name, ApplicationElementAttributes) class AddressbookKey(object): def __init__(self, section): self.group = 'Addressbook' self.section = section def __get__(self, obj, objtype): if obj is None: return [self.group, self.section] else: return [self.group, self.section, PersistentKey(obj.__id__)] def __set__(self, obj, value): raise AttributeError('cannot set attribute') def __delete__(self, obj): raise AttributeError('cannot delete attribute') class MultiAccountTransaction(object): def __init__(self, accounts): self.accounts = accounts def __enter__(self): for account in self.accounts: account.xcap_manager.start_transaction() return self def __exit__(self, exc_type, exc_value, traceback): for account in self.accounts: account.xcap_manager.commit_transaction() def __iter__(self): return iter(self.accounts) class XCAPGroup(xcap.Group): """An XCAP Group with attributes normalized to unicode""" __attributes__ = set() def __init__(self, id, name, contacts, **attributes): normalized_attributes = dict((name, str(value) if value is not None else None) for name, value in list(attributes.items()) if name in self.__attributes__) contacts = [XCAPContact.normalize(contact) for contact in contacts] super(XCAPGroup, self).__init__(id, name, contacts, **normalized_attributes) @classmethod def normalize(cls, group): return cls(group.id, group.name, group.contacts, **group.attributes) def get_modified(self, modified_keys): names = {'name'} attributes = dict((name, getattr(self, name)) for name in names.intersection(modified_keys)) attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys)) return attributes class XCAPContactURI(xcap.ContactURI): """An XCAP ContactURI with attributes normalized to unicode""" __attributes__ = set() def __init__(self, id, uri, type, **attributes): normalized_attributes = dict((name, str(value) if value is not None else None) for name, value in list(attributes.items()) if name in self.__attributes__) super(XCAPContactURI, self).__init__(id, uri, type, **normalized_attributes) @classmethod def normalize(cls, uri): return cls(uri.id, uri.uri, uri.type, **uri.attributes) def get_modified(self, modified_keys): names = {'uri', 'type'} attributes = dict((name, getattr(self, name)) for name in names.intersection(modified_keys)) attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys)) return attributes class XCAPContact(xcap.Contact): """An XCAP Contact with attributes normalized to unicode""" __attributes__ = set() def __init__(self, id, name, uris, presence_handling=None, dialog_handling=None, **attributes): normalized_attributes = dict((name, str(value) if value is not None else None) for name, value in list(attributes.items()) if name in self.__attributes__) uris = xcap.ContactURIList((XCAPContactURI.normalize(uri) for uri in uris), default=getattr(uris, 'default', None)) super(XCAPContact, self).__init__(id, name, uris, presence_handling, dialog_handling, **normalized_attributes) @classmethod def normalize(cls, contact): return cls(contact.id, contact.name, contact.uris, contact.presence, contact.dialog, **contact.attributes) def get_modified(self, modified_keys): names = {'name', 'uris.default', 'presence.policy', 'presence.subscribe', 'dialog.policy', 'dialog.subscribe'} attributes = dict((name, recursive_getattr(self, name)) for name in names.intersection(modified_keys)) attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys)) return attributes class XCAPPolicy(xcap.Policy): """An XCAP Policy with attributes normalized to unicode""" __attributes__ = set() def __init__(self, id, uri, name, presence_handling=None, dialog_handling=None, **attributes): normalized_attributes = dict((name, str(value) if value is not None else None) for name, value in list(attributes.items()) if name in self.__attributes__) super(XCAPPolicy, self).__init__(id, uri, name, presence_handling, dialog_handling, **normalized_attributes) @classmethod def normalize(cls, policy): return cls(policy.id, policy.uri, policy.name, policy.presence, policy.dialog, **policy.attributes) def get_modified(self, modified_keys): names = {'uri', 'name', 'presence.policy', 'presence.subscribe', 'dialog.policy', 'dialog.subscribe'} attributes = dict((name, recursive_getattr(self, name)) for name in names.intersection(modified_keys)) attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys)) return attributes class ContactListDescriptor(AbstractSetting): def __init__(self): self.values = defaultweakobjectmap(ContactList) self.oldvalues = defaultweakobjectmap(ContactList) self.lock = Lock() def __get__(self, obj, objtype): if obj is None: return self with self.lock: return self.values[obj] def __set__(self, obj, value): if value is None: raise ValueError("setting attribute is not nillable") elif not isinstance(value, ContactList): value = ContactList(value) with self.lock: self.values[obj] = value def __getstate__(self, obj): with self.lock: return self.values[obj].__getstate__() def __setstate__(self, obj, value): if value is None: raise ValueError("setting attribute is not nillable") object = ContactList.__new__(ContactList) object.__setstate__(value) with self.lock: self.values[obj] = object self.oldvalues[obj] = ContactList(object) def get_modified(self, obj): with self.lock: old = self.oldvalues[obj] new = self.values[obj] with new.lock: old_ids = set(old.ids()) new_ids = set(new.ids()) added_contacts = [new[id] for id in new_ids - old_ids] removed_contacts = [old[id] for id in old_ids - new_ids] try: if added_contacts or removed_contacts: return ModifiedList(added=added_contacts, removed=removed_contacts, modified=None) else: return None finally: self.oldvalues[obj] = ContactList(new) def get_old(self, obj): with self.lock: return self.oldvalues[obj] def undo(self, obj): with self.lock: self.values[obj] = ContactList(self.oldvalues[obj]) class ContactList(object): def __new__(cls, contacts=None): instance = object.__new__(cls) instance.lock = Lock() return instance def __init__(self, contacts=None): self.contacts = dict((contact.id, contact) for contact in contacts or [] if contact.__state__ != 'deleted') def __getitem__(self, key): return self.contacts[key] def __contains__(self, key): return key in self.contacts def __iter__(self): return iter(sorted(list(self.contacts.values()), key=attrgetter('id'))) def __reversed__(self): return iter(sorted(list(self.contacts.values()), key=attrgetter('id'), reverse=True)) __hash__ = None def __len__(self): return len(self.contacts) def __eq__(self, other): if isinstance(other, ContactList): return self.contacts == other.contacts 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)" % (self.__class__.__name__, sorted(list(self.contacts.values()), key=attrgetter('id'))) def __getstate__(self): return list(self.contacts.keys()) def __setstate__(self, value): addressbook_manager = AddressbookManager() for id in [id for id in value if not addressbook_manager.has_contact(id)]: value.remove(id) with self.lock: self.contacts = dict((id, addressbook_manager.get_contact(id)) for id in value) def ids(self): return sorted(self.contacts.keys()) def add(self, contact): if contact.__state__ == 'deleted': return with self.lock: self.contacts[contact.id] = contact def remove(self, contact): with self.lock: self.contacts.pop(contact.id, None) class Group(SettingsState): __key__ = AddressbookKey('Groups') __id__ = SettingsObjectImmutableID(type=ID) id = __id__ name = Setting(type=str, default='') contacts = ContactListDescriptor() def __new__(cls, id=None): - with AddressbookManager.load.lock: - if not AddressbookManager.load.called: - raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__) +# with AddressbookManager.load.lock: +# if not AddressbookManager.load.was_called: +# raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__) if id is None: id = unique_id() elif not isinstance(id, str): raise TypeError("id needs to be a string or unicode object") instance = SettingsState.__new__(cls) instance.__id__ = id instance.__state__ = 'new' instance.__xcapgroup__ = None configuration = ConfigurationManager() try: data = configuration.get(instance.__key__) except ObjectNotFoundError: pass else: instance.__setstate__(data) instance.__state__ = 'loaded' instance.__xcapgroup__ = instance.__toxcap__() return instance def __establish__(self): if self.__state__ == 'loaded': self.__state__ = 'active' notification_center = NotificationCenter() notification_center.post_notification('AddressbookGroupWasActivated', sender=self) def __repr__(self): return "%s(id=%r)" % (self.__class__.__name__, self.id) def __toxcap__(self): xcap_contacts = [contact.__xcapcontact__ for contact in self.contacts] attributes = dict((name, getattr(self, name)) for name, attr in list(vars(self.__class__).items()) if isinstance(attr, SharedSetting)) return XCAPGroup(self.id, self.name, xcap_contacts, **attributes) @run_in_thread('file-io') def _internal_save(self, originator): if self.__state__ == 'deleted': return for contact in [contact for contact in self.contacts if contact.__state__ == 'deleted']: self.contacts.remove(contact) modified_settings = self.get_modified() if not modified_settings and self.__state__ != 'new': return account_manager = AccountManager() configuration = ConfigurationManager() notification_center = NotificationCenter() if originator is Local: originator_account = None previous_xcapgroup = self.__xcapgroup__ else: originator_account = originator.account previous_xcapgroup = originator.xcap_object xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] self.__xcapgroup__ = self.__toxcap__() if self.__state__ == 'new': configuration.update(self.__key__, self.__getstate__()) self.__state__ = 'active' for account in (account for account in xcap_accounts if account is not originator_account): account.xcap_manager.add_group(self.__xcapgroup__) modified_data = None notification_center.post_notification('AddressbookGroupWasActivated', sender=self) notification_center.post_notification('AddressbookGroupWasCreated', sender=self) elif all(isinstance(self.__settings__[key], RuntimeSetting) for key in modified_settings): notification_center.post_notification('AddressbookGroupDidChange', sender=self, data=NotificationData(modified=modified_settings)) return else: configuration.update(self.__key__, self.__getstate__()) attributes = self.__xcapgroup__.get_modified(modified_settings) if 'contacts' in modified_settings: added_contacts = [contact.__xcapcontact__ for contact in modified_settings['contacts'].added] removed_contacts = [contact.__xcapcontact__ for contact in modified_settings['contacts'].removed] else: added_contacts = [] removed_contacts = [] if self.__xcapgroup__ != previous_xcapgroup: outofsync_accounts = xcap_accounts elif originator is Local: outofsync_accounts = [] else: outofsync_accounts = list(account for account in xcap_accounts if account is not originator_account) with MultiAccountTransaction(outofsync_accounts): for account in outofsync_accounts: xcap_manager = account.xcap_manager for xcapcontact in added_contacts: xcap_manager.add_group_member(self.__xcapgroup__, xcapcontact) for xcapcontact in removed_contacts: xcap_manager.remove_group_member(self.__xcapgroup__, xcapcontact) if attributes: xcap_manager.update_group(self.__xcapgroup__, attributes) notification_center.post_notification('AddressbookGroupDidChange', sender=self, data=NotificationData(modified=modified_settings)) modified_data = modified_settings try: configuration.save() except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='save', modified=modified_data, exception=e)) @run_in_thread('file-io') def _internal_delete(self, originator): if self.__state__ == 'deleted': return self.__state__ = 'deleted' configuration = ConfigurationManager() account_manager = AccountManager() notification_center = NotificationCenter() if originator is Local: originator_account = None else: originator_account = originator.account configuration.delete(self.__key__) for account in (account for account in account_manager.iter_accounts() if account.xcap.discovered and account is not originator_account): account.xcap_manager.remove_group(self.__xcapgroup__) notification_center.post_notification('AddressbookGroupWasDeleted', sender=self) try: configuration.save() except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='delete', exception=e)) def save(self): """ Store the group into persistent storage (local and xcap). This method will post the AddressbookGroupWasCreated and AddressbookGroupWasActivated notifications on the first save or a AddressbookGroupDidChange notification on subsequent saves, regardless of whether the contact has been saved to persistent storage or not. A CFGManagerSaveFailed notification is posted if saving to the persistent configuration storage fails. """ self._internal_save(originator=Local) def delete(self): """Remove the group from the persistent storage.""" self._internal_delete(originator=Local) def clone(self, new_id=None): """Create a copy of this group and all its sub-settings.""" raise NotImplementedError @classmethod def register_extension(cls, extension): """ Register an extension for this class. All Settings and SettingsGroups defined in the extension will be added to this class, overwriting any attributes with the same name. Other attributes in the extension are ignored. """ if not issubclass(extension, GroupExtension): raise TypeError("expected subclass of GroupExtension, got %r" % (extension,)) for name in dir(extension): attribute = getattr(extension, name, None) if isinstance(attribute, SharedSetting): if SharedSetting.__namespace__ is None: raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace") XCAPGroup.__attributes__.add(name) if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)): setattr(cls, name, attribute) class GroupExtension(object): """Base class for extensions of Groups""" def __new__(cls, *args, **kw): raise TypeError("GroupExtension subclasses cannot be instantiated") class ContactURI(SettingsState): __id__ = SettingsObjectImmutableID(type=ID) id = __id__ uri = Setting(type=str, default='') type = Setting(type=str, default=None, nillable=True) def __new__(cls, id=None, **state): if id is None: id = unique_id() elif not isinstance(id, str): raise TypeError("id needs to be a string or unicode object") instance = SettingsState.__new__(cls) instance.__id__ = id instance.__setstate__(state) return instance def __repr__(self): return "%s(id=%r)" % (self.__class__.__name__, self.id) def __toxcap__(self): attributes = dict((name, getattr(self, name)) for name, attr in list(vars(self.__class__).items()) if isinstance(attr, SharedSetting)) return XCAPContactURI(self.id, self.uri, self.type, **attributes) @classmethod def register_extension(cls, extension): """ Register an extension for this class. All Settings and SettingsGroups defined in the extension will be added to this class, overwriting any attributes with the same name. Other attributes in the extension are ignored. """ if not issubclass(extension, ContactURIExtension): raise TypeError("expected subclass of ContactURIExtension, got %r" % (extension,)) for name in dir(extension): attribute = getattr(extension, name, None) if isinstance(attribute, SharedSetting): if SharedSetting.__namespace__ is None: raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace") XCAPContactURI.__attributes__.add(name) if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)): setattr(cls, name, attribute) class ContactURIExtension(object): """Base class for extensions of ContactURIs""" def __new__(cls, *args, **kw): raise TypeError("ContactURIExtension subclasses cannot be instantiated") class DefaultContactURI(Setting): def __init__(self): super(DefaultContactURI, self).__init__(type=str, default=None, nillable=True) def __get__(self, obj, objtype): value = super(DefaultContactURI, self).__get__(obj, objtype) return value if value in (self, None) else obj._item_map.get(value) def __set__(self, obj, value): if value is not None: if not isinstance(value, ContactURI): raise TypeError("the default URI must be a ContactURI instance or None") with obj._lock: if value.id not in obj._item_map: raise ValueError("the default URI can only be set to one of the URIs of the contact") super(DefaultContactURI, self).__set__(obj, value.id) else: super(DefaultContactURI, self).__set__(obj, None) def get_modified(self, obj): modified_value = super(DefaultContactURI, self).get_modified(obj) if modified_value is not None: old_uri = obj._item_map.old.get(modified_value.old) if modified_value.old is not None else None new_uri = obj._item_map.get(modified_value.new) if modified_value.new is not None else None modified_value = ModifiedValue(old=old_uri, new=new_uri) return modified_value def get_old(self, obj): value = super(DefaultContactURI, self).get_old(obj) return value if value is None else obj._item_map.old.get(value) class ContactURIManagement(ItemManagement): def remove_item(self, item, collection): if collection.default is item: collection.default = None def set_items(self, items, collection): if collection.default is not None and collection.default not in items: collection.default = None class ContactURIList(ItemCollection): _item_type = ContactURI _item_management = ContactURIManagement() default = DefaultContactURI() class DialogSettings(SettingsGroup): policy = Setting(type=PolicyValue, default='default') subscribe = Setting(type=bool, default=False) class PresenceSettings(SettingsGroup): policy = Setting(type=PolicyValue, default='default') subscribe = Setting(type=bool, default=False) class Contact(SettingsState): __key__ = AddressbookKey('Contacts') __id__ = SettingsObjectImmutableID(type=ID) id = __id__ name = Setting(type=str, default='') uris = ContactURIList dialog = DialogSettings presence = PresenceSettings def __new__(cls, id=None): - with AddressbookManager.load.lock: - if not AddressbookManager.load.called: - raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__) +# with AddressbookManager.load.lock: +# if not AddressbookManager.load.was_called: +# raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__) if id is None: id = unique_id() elif not isinstance(id, str): raise TypeError("id needs to be a string or unicode object") instance = SettingsState.__new__(cls) instance.__id__ = id instance.__state__ = 'new' instance.__xcapcontact__ = None configuration = ConfigurationManager() try: data = configuration.get(instance.__key__) except ObjectNotFoundError: pass else: instance.__setstate__(data) instance.__state__ = 'loaded' instance.__xcapcontact__ = instance.__toxcap__() return instance def __establish__(self): if self.__state__ == 'loaded': self.__state__ = 'active' notification_center = NotificationCenter() notification_center.post_notification('AddressbookContactWasActivated', sender=self) def __repr__(self): return "%s(id=%r)" % (self.__class__.__name__, self.id) def __toxcap__(self): contact_uris = xcap.ContactURIList((uri.__toxcap__() for uri in self.uris), default=self.uris.default.id if self.uris.default is not None else None) dialog_handling = xcap.EventHandling(self.dialog.policy, self.dialog.subscribe) presence_handling = xcap.EventHandling(self.presence.policy, self.presence.subscribe) attributes = dict((name, getattr(self, name)) for name, attr in list(vars(self.__class__).items()) if isinstance(attr, SharedSetting)) return XCAPContact(self.id, self.name, contact_uris, presence_handling, dialog_handling, **attributes) @run_in_thread('file-io') def _internal_save(self, originator): if self.__state__ == 'deleted': return modified_settings = self.get_modified() if not modified_settings and self.__state__ != 'new': return account_manager = AccountManager() configuration = ConfigurationManager() notification_center = NotificationCenter() if originator is Local: originator_account = None previous_xcapcontact = self.__xcapcontact__ else: originator_account = originator.account previous_xcapcontact = originator.xcap_object xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] self.__xcapcontact__ = self.__toxcap__() if self.__state__ == 'new': configuration.update(self.__key__, self.__getstate__()) self.__state__ = 'active' for account in (account for account in xcap_accounts if account is not originator_account): account.xcap_manager.add_contact(self.__xcapcontact__) modified_data = None notification_center.post_notification('AddressbookContactWasActivated', sender=self) notification_center.post_notification('AddressbookContactWasCreated', sender=self) elif all(isinstance(self.__settings__[key], RuntimeSetting) for key in modified_settings): notification_center.post_notification('AddressbookContactDidChange', sender=self, data=NotificationData(modified=modified_settings)) return else: configuration.update(self.__key__, self.__getstate__()) contact_attributes = self.__xcapcontact__.get_modified(modified_settings) if 'uris' in modified_settings: xcap_uris = self.__xcapcontact__.uris added_uris = [xcap_uris[uri.id] for uri in modified_settings['uris'].added] removed_uris = [uri.__toxcap__() for uri in modified_settings['uris'].removed] modified_uris = dict((xcap_uris[id], xcap_uris[id].get_modified(changemap)) for id, changemap in list(modified_settings['uris'].modified.items())) else: added_uris = [] removed_uris = [] modified_uris = {} if self.__xcapcontact__ != previous_xcapcontact: outofsync_accounts = xcap_accounts elif originator is Local: outofsync_accounts = [] else: outofsync_accounts = list(account for account in xcap_accounts if account is not originator_account) with MultiAccountTransaction(outofsync_accounts): for account in outofsync_accounts: xcap_manager = account.xcap_manager for xcapuri in added_uris: xcap_manager.add_contact_uri(self.__xcapcontact__, xcapuri) for xcapuri in removed_uris: xcap_manager.remove_contact_uri(self.__xcapcontact__, xcapuri) for xcapuri, uri_attributes in list(modified_uris.items()): xcap_manager.update_contact_uri(self.__xcapcontact__, xcapuri, uri_attributes) if contact_attributes: xcap_manager.update_contact(self.__xcapcontact__, contact_attributes) notification_center.post_notification('AddressbookContactDidChange', sender=self, data=NotificationData(modified=modified_settings)) modified_data = modified_settings try: configuration.save() except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='save', modified=modified_data, exception=e)) @run_in_thread('file-io') def _internal_delete(self, originator): if self.__state__ == 'deleted': return self.__state__ = 'deleted' configuration = ConfigurationManager() account_manager = AccountManager() addressbook_manager = AddressbookManager() notification_center = NotificationCenter() if originator is Local: originator_account = None else: originator_account = originator.account configuration.delete(self.__key__) xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] with MultiAccountTransaction(xcap_accounts): for group in (group for group in addressbook_manager.get_groups() if self.id in group.contacts): group.contacts.remove(self) group.save() for account in (account for account in xcap_accounts if account is not originator_account): account.xcap_manager.remove_contact(self.__xcapcontact__) notification_center.post_notification('AddressbookContactWasDeleted', sender=self) try: configuration.save() except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='delete', exception=e)) def save(self): """ Store the contact into persistent storage (local and xcap). This method will post the AddressbookContactWasCreated and AddressbookContactWasActivated notifications on the first save or a AddressbookContactDidChange notification on subsequent saves, regardless of whether the contact has been saved to persistent storage or not. A CFGManagerSaveFailed notification is posted if saving to the persistent configuration storage fails. """ self._internal_save(originator=Local) def delete(self): """Remove the contact from the persistent storage.""" self._internal_delete(originator=Local) def clone(self, new_id=None): """Create a copy of this contact and all its sub-settings.""" raise NotImplementedError @classmethod def register_extension(cls, extension): """ Register an extension for this class. All Settings and SettingsGroups defined in the extension will be added to this class, overwriting any attributes with the same name. Other attributes in the extension are ignored. """ if not issubclass(extension, ContactExtension): raise TypeError("expected subclass of ContactExtension, got %r" % (extension,)) for name in dir(extension): attribute = getattr(extension, name, None) if isinstance(attribute, SharedSetting): if SharedSetting.__namespace__ is None: raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace") XCAPContact.__attributes__.add(name) if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)): setattr(cls, name, attribute) class ContactExtension(object): """Base class for extensions of Contacts""" def __new__(cls, *args, **kw): raise TypeError("ContactExtension subclasses cannot be instantiated") class Policy(SettingsState): __key__ = AddressbookKey('Policies') __id__ = SettingsObjectImmutableID(type=ID) id = __id__ uri = Setting(type=str, default='') name = Setting(type=str, default='') dialog = DialogSettings presence = PresenceSettings def __new__(cls, id=None): - with AddressbookManager.load.lock: - if not AddressbookManager.load.called: - raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__) +# with AddressbookManager.load.lock: +# if not AddressbookManager.load.was_called: +# raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__) if id is None: id = unique_id() elif not isinstance(id, str): raise TypeError("id needs to be a string or unicode object") instance = SettingsState.__new__(cls) instance.__id__ = id instance.__state__ = 'new' instance.__xcappolicy__ = None configuration = ConfigurationManager() try: data = configuration.get(instance.__key__) except ObjectNotFoundError: pass else: instance.__setstate__(data) instance.__state__ = 'loaded' instance.__xcappolicy__ = instance.__toxcap__() return instance def __establish__(self): if self.__state__ == 'loaded': self.__state__ = 'active' notification_center = NotificationCenter() notification_center.post_notification('AddressbookPolicyWasActivated', sender=self) def __repr__(self): return "%s(id=%r)" % (self.__class__.__name__, self.id) def __toxcap__(self): dialog_handling = xcap.EventHandling(self.dialog.policy, self.dialog.subscribe) presence_handling = xcap.EventHandling(self.presence.policy, self.presence.subscribe) attributes = dict((name, getattr(self, name)) for name, attr in list(vars(self.__class__).items()) if isinstance(attr, SharedSetting)) return XCAPPolicy(self.id, self.uri, self.name, presence_handling, dialog_handling, **attributes) @run_in_thread('file-io') def _internal_save(self, originator): if self.__state__ == 'deleted': return modified_settings = self.get_modified() if not modified_settings and self.__state__ != 'new': return account_manager = AccountManager() configuration = ConfigurationManager() notification_center = NotificationCenter() if originator is Local: originator_account = None previous_xcappolicy = self.__xcappolicy__ else: originator_account = originator.account previous_xcappolicy = originator.xcap_object xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] self.__xcappolicy__ = self.__toxcap__() if self.__state__ == 'new': configuration.update(self.__key__, self.__getstate__()) self.__state__ = 'active' for account in (account for account in xcap_accounts if account is not originator_account): account.xcap_manager.add_policy(self.__xcappolicy__) modified_data = None notification_center.post_notification('AddressbookPolicyWasActivated', sender=self) notification_center.post_notification('AddressbookPolicyWasCreated', sender=self) elif all(isinstance(self.__settings__[key], RuntimeSetting) for key in modified_settings): notification_center.post_notification('AddressbookPolicyDidChange', sender=self, data=NotificationData(modified=modified_settings)) return else: configuration.update(self.__key__, self.__getstate__()) attributes = self.__xcappolicy__.get_modified(modified_settings) if self.__xcappolicy__ != previous_xcappolicy: outofsync_accounts = xcap_accounts elif originator is Local: outofsync_accounts = [] else: outofsync_accounts = list(account for account in xcap_accounts if account is not originator_account) for account in outofsync_accounts: account.xcap_manager.update_policy(self.__xcappolicy__, attributes) notification_center.post_notification('AddressbookPolicyDidChange', sender=self, data=NotificationData(modified=modified_settings)) modified_data = modified_settings try: configuration.save() except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='save', modified=modified_data, exception=e)) @run_in_thread('file-io') def _internal_delete(self, originator): if self.__state__ == 'deleted': return self.__state__ = 'deleted' configuration = ConfigurationManager() account_manager = AccountManager() notification_center = NotificationCenter() if originator is Local: originator_account = None else: originator_account = originator.account configuration.delete(self.__key__) for account in (account for account in account_manager.iter_accounts() if account.xcap.discovered and account is not originator_account): account.xcap_manager.remove_policy(self.__xcappolicy__) notification_center.post_notification('AddressbookPolicyWasDeleted', sender=self) try: configuration.save() except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='delete', exception=e)) def save(self): """ Store the policy into persistent storage (local and xcap). It will post the AddressbookPolicyWasCreated and AddressbookPolicyWasActivated notifications on the first save or a AddressbookPolicyDidChange notification on subsequent saves, regardless of whether the policy has been saved to persistent storage or not. A CFGManagerSaveFailed notification is posted if saving to the persistent configuration storage fails. """ self._internal_save(originator=Local) def delete(self): """Remove the policy from the persistent storage.""" self._internal_delete(originator=Local) def clone(self, new_id=None): """Create a copy of this policy and all its sub-settings.""" raise NotImplementedError @classmethod def register_extension(cls, extension): """ Register an extension for this class. All Settings and SettingsGroups defined in the extension will be added to this class, overwriting any attributes with the same name. Other attributes in the extension are ignored. """ if not issubclass(extension, PolicyExtension): raise TypeError("expected subclass of PolicyExtension, got %r" % (extension,)) for name in dir(extension): attribute = getattr(extension, name, None) if isinstance(attribute, SharedSetting): if SharedSetting.__namespace__ is None: raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace") XCAPPolicy.__attributes__.add(name) if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)): setattr(cls, name, attribute) class PolicyExtension(object): """Base class for extensions of Policies""" def __new__(cls, *args, **kw): raise TypeError("PolicyExtension subclasses cannot be instantiated") @implementer(IObserver) class AddressbookManager(object, metaclass=Singleton): def __init__(self): self.contacts = {} self.groups = {} self.policies = {} self.__xcapaddressbook__ = None notification_center = NotificationCenter() notification_center.add_observer(self, name='AddressbookContactWasActivated') notification_center.add_observer(self, name='AddressbookContactWasDeleted') notification_center.add_observer(self, name='AddressbookGroupWasActivated') notification_center.add_observer(self, name='AddressbookGroupWasDeleted') notification_center.add_observer(self, name='AddressbookPolicyWasActivated') notification_center.add_observer(self, name='AddressbookPolicyWasDeleted') notification_center.add_observer(self, name='SIPAccountDidDiscoverXCAPSupport') notification_center.add_observer(self, name='XCAPManagerDidReloadData') @execute_once def load(self): configuration = ConfigurationManager() # temporary workaround to migrate contacts to the new format. to be removed later. -Dan if 'Contacts' in configuration.data or 'ContactGroups' in configuration.data: account_manager = AccountManager() old_data = dict(contacts=configuration.data.pop('Contacts', {}), groups=configuration.data.pop('ContactGroups', {})) if any(account.enabled and account.xcap.enabled and account.xcap.discovered for account in account_manager.get_accounts()): self.__old_data = old_data else: self.__migrate_contacts(old_data) return [Contact(id=id) for id in configuration.get_names(Contact.__key__)] [Group(id=id) for id in configuration.get_names(Group.__key__)] [Policy(id=id) for id in configuration.get_names(Policy.__key__)] def start(self): pass def stop(self): pass def has_contact(self, id): return id in self.contacts def get_contact(self, id): return self.contacts[id] def get_contacts(self): return list(self.contacts.values()) def has_group(self, id): return id in self.groups def get_group(self, id): return self.groups[id] def get_groups(self): return list(self.groups.values()) def has_policy(self, id): return id in self.policies def get_policy(self, id): return self.policies[id] def get_policies(self): return list(self.policies.values()) @classmethod def transaction(cls): account_manager = AccountManager() xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] return MultiAccountTransaction(xcap_accounts) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_AddressbookContactWasActivated(self, notification): contact = notification.sender self.contacts[contact.id] = contact notification.center.post_notification('AddressbookManagerDidAddContact', sender=self, data=NotificationData(contact=contact)) def _NH_AddressbookContactWasDeleted(self, notification): contact = notification.sender del self.contacts[contact.id] notification.center.post_notification('AddressbookManagerDidRemoveContact', sender=self, data=NotificationData(contact=contact)) def _NH_AddressbookGroupWasActivated(self, notification): group = notification.sender self.groups[group.id] = group notification.center.post_notification('AddressbookManagerDidAddGroup', sender=self, data=NotificationData(group=group)) def _NH_AddressbookGroupWasDeleted(self, notification): group = notification.sender del self.groups[group.id] notification.center.post_notification('AddressbookManagerDidRemoveGroup', sender=self, data=NotificationData(group=group)) def _NH_AddressbookPolicyWasActivated(self, notification): policy = notification.sender self.policies[policy.id] = policy notification.center.post_notification('AddressbookManagerDidAddPolicy', sender=self, data=NotificationData(policy=policy)) def _NH_AddressbookPolicyWasDeleted(self, notification): policy = notification.sender del self.policies[policy.id] notification.center.post_notification('AddressbookManagerDidRemovePolicy', sender=self, data=NotificationData(policy=policy)) @run_in_thread('file-io') def _NH_SIPAccountDidDiscoverXCAPSupport(self, notification): xcap_manager = notification.sender.xcap_manager with xcap_manager.transaction(): for contact in list(self.contacts.values()): xcap_manager.add_contact(contact.__xcapcontact__) for group in list(self.groups.values()): xcap_manager.add_group(group.__xcapgroup__) for policy in list(self.policies.values()): xcap_manager.add_policy(policy.__xcappolicy__) @run_in_thread('file-io') def _NH_XCAPManagerDidReloadData(self, notification): if notification.data.addressbook == self.__xcapaddressbook__: return self.__xcapaddressbook__ = notification.data.addressbook xcap_manager = notification.sender xcap_contacts = notification.data.addressbook.contacts xcap_groups = notification.data.addressbook.groups xcap_policies = notification.data.addressbook.policies account_manager = AccountManager() xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] # temporary workaround to migrate contacts to the new format. to be removed later. -Dan if hasattr(self, '_AddressbookManager__old_data'): old_data = self.__old_data del self.__old_data if not xcap_contacts and not xcap_groups: self.__migrate_contacts(old_data) return with MultiAccountTransaction(xcap_accounts): # because groups depend on contacts, operation order is add/update contacts, add/update/remove groups & policies, remove contacts -Dan for xcap_contact in xcap_contacts: xcap_contact = XCAPContact.normalize(xcap_contact) try: contact = self.contacts[xcap_contact.id] except KeyError: try: contact = Contact(xcap_contact.id) except DuplicateIDError: log.exception() continue contact.name = xcap_contact.name contact.presence.policy = xcap_contact.presence.policy contact.presence.subscribe = xcap_contact.presence.subscribe contact.dialog.policy = xcap_contact.dialog.policy contact.dialog.subscribe = xcap_contact.dialog.subscribe for name, value in list(xcap_contact.attributes.items()): setattr(contact, name, value) for xcap_uri in xcap_contact.uris: xcap_uri = XCAPContactURI.normalize(xcap_uri) try: uri = contact.uris[xcap_uri.id] except KeyError: try: uri = ContactURI(xcap_uri.id) except DuplicateIDError: log.exception() continue contact.uris.add(uri) uri.uri = xcap_uri.uri uri.type = xcap_uri.type for name, value in list(xcap_uri.attributes.items()): setattr(uri, name, value) for uri in (uri for uri in list(contact.uris) if uri.id not in xcap_contact.uris): contact.uris.remove(uri) contact.uris.default = contact.uris.get(xcap_contact.uris.default, None) contact._internal_save(originator=Remote(xcap_manager.account, xcap_contact)) for xcap_group in xcap_groups: xcap_group = XCAPGroup.normalize(xcap_group) try: group = self.groups[xcap_group.id] except KeyError: try: group = Group(xcap_group.id) except DuplicateIDError: log.exception() continue group.name = xcap_group.name for name, value in list(xcap_group.attributes.items()): setattr(group, name, value) old_contact_ids = set(group.contacts.ids()) new_contact_ids = set(xcap_group.contacts.ids()) for contact in (self.contacts[id] for id in new_contact_ids - old_contact_ids): group.contacts.add(contact) for contact in (group.contacts[id] for id in old_contact_ids - new_contact_ids): group.contacts.remove(contact) group._internal_save(originator=Remote(xcap_manager.account, xcap_group)) for xcap_policy in xcap_policies: xcap_policy = XCAPPolicy.normalize(xcap_policy) try: policy = self.policies[xcap_policy.id] except KeyError: try: policy = Policy(xcap_policy.id) except DuplicateIDError: log.exception() continue policy.uri = xcap_policy.uri policy.name = xcap_policy.name policy.presence.policy = xcap_policy.presence.policy policy.presence.subscribe = xcap_policy.presence.subscribe policy.dialog.policy = xcap_policy.dialog.policy policy.dialog.subscribe = xcap_policy.dialog.subscribe for name, value in list(xcap_policy.attributes.items()): setattr(policy, name, value) policy._internal_save(originator=Remote(xcap_manager.account, xcap_policy)) originator = Remote(xcap_manager.account, None) for policy in (policy for policy in list(self.policies.values()) if policy.id not in xcap_policies): policy._internal_delete(originator=originator) for group in (group for group in list(self.groups.values()) if group.id not in xcap_groups): group._internal_delete(originator=originator) for contact in (contact for contact in list(self.contacts.values()) if contact.id not in xcap_contacts): contact._internal_delete(originator=originator) def __migrate_contacts(self, old_data): account_manager = AccountManager() xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] with MultiAccountTransaction(xcap_accounts): # restore the old contacts and groups old_groups = old_data['groups'] old_contacts = old_data['contacts'] group_idmap = {} for group_id, group_state in list(old_groups.items()): group_idmap[group_id] = group = Group() for name, value in list(group_state.items()): try: setattr(group, name, value) except (ValueError, TypeError): pass for account_id, account_contacts in list(old_contacts.items()): for group_id, contact_map in list(account_contacts.items()): for uri, contact_data in list(contact_map.items()): contact = Contact() for name, value in list(contact_data.items()): try: setattr(contact, name, value) except (ValueError, TypeError): pass contact.uris.add(ContactURI(uri=uri)) contact.save() group = group_idmap.get(group_id, Null) group.contacts.add(contact) for group in list(group_idmap.values()): group.save() diff --git a/sipsimple/util/__init__.py b/sipsimple/util/__init__.py index 82b9fb18..af49a9a3 100644 --- a/sipsimple/util/__init__.py +++ b/sipsimple/util/__init__.py @@ -1,146 +1,155 @@ """Implements utilities commonly used in various parts of the library""" -__all__ = ["All", "Any", "ExponentialTimer", "ISOTimestamp", "MultilingualText", "user_info", "sha1"] +__all__ = ["All", "Any", "ExponentialTimer", "ISOTimestamp", "MultilingualText", "user_info", "sha1", "execute_once"] import os import platform import sys import dateutil.parser from application.notification import NotificationCenter from application.python.types import Singleton, MarkerType from datetime import datetime from dateutil.tz import tzlocal, tzutc from twisted.internet import reactor from sipsimple.util._sha1 import sha1 # Utility classes # class All(object, metaclass=MarkerType): pass class Any(object, metaclass=MarkerType): pass class ISOTimestamp(datetime): def __new__(cls, *args, **kw): if len(args) == 1: value = args[0] if isinstance(value, cls): return value elif isinstance(value, str): value = dateutil.parser.parse(value) return cls(value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond, value.tzinfo) elif isinstance(value, datetime): return cls(value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond, value.tzinfo or tzlocal()) else: return datetime.__new__(cls, *args, **kw) else: if len(args) < 8 and 'tzinfo' not in kw: kw['tzinfo'] = tzlocal() return datetime.__new__(cls, *args, **kw) def __str__(self): return self.isoformat() @classmethod def now(cls): return cls(datetime.now(tzlocal())) @classmethod def utcnow(cls): return cls(datetime.now(tzutc())) class MultilingualText(str): def __new__(cls, *args, **translations): if len(args) > 1: raise TypeError("%s.__new__ takes at most 1 positional argument (%d given)" % (cls.__name__, len(args))) default = args[0] if args else translations.get('en', '') obj = str.__new__(cls, default) obj.translations = translations return obj def get_translation(self, language): return self.translations.get(language, self) class ExponentialTimer(object): def __init__(self): self._timer = None self._limit_timer = None self._interval = 0 self._iterations = None def _step(self): if self._iterations is not None: self._iterations -= 1 if self._iterations == 0: self.stop() else: self._interval *= 2 self._timer = reactor.callLater(self._interval, self._step) NotificationCenter().post_notification('ExponentialTimerDidTimeout', sender=self) @property def active(self): return self._timer is not None def start(self, base_interval, immediate=False, iterations=None, time_limit=None): assert base_interval > 0 assert iterations is None or iterations > 0 assert time_limit is None or time_limit > 0 if self._timer is not None: self.stop() self._interval = base_interval / 2.0 if immediate else base_interval self._iterations = iterations if time_limit is not None: self._limit_timer = reactor.callLater(time_limit, self.stop) self._timer = reactor.callLater(0 if immediate else base_interval, self._step) def stop(self): if self._timer is not None and self._timer.active(): self._timer.cancel() if self._limit_timer is not None and self._limit_timer.active(): self._limit_timer.cancel() self._timer = None self._limit_timer = None # Utility objects # class UserInfo(object, metaclass=Singleton): def __repr__(self): return ''.format(self) @property def username(self): if platform.system() == 'Windows': name = os.getenv('USERNAME') else: import pwd name = pwd.getpwuid(os.getuid()).pw_name return name.decode(sys.getfilesystemencoding()) @property def fullname(self): if platform.system() == 'Windows': name = os.getenv('USERNAME') else: import pwd name = pwd.getpwuid(os.getuid()).pw_gecos.split(',', 1)[0] or pwd.getpwuid(os.getuid()).pw_name return name.decode(sys.getfilesystemencoding()) user_info = UserInfo() del UserInfo +def execute_once(func): + def wrapper(*args, **kwargs): + if not wrapper.has_run: + wrapper.has_run = True + return func(*args, **kwargs) + wrapper.has_run = False + return wrapper + +