diff --git a/sipsimple/addressbook.py b/sipsimple/addressbook.py index e054df7f..73ad5187 100644 --- a/sipsimple/addressbook.py +++ b/sipsimple/addressbook.py @@ -1,1365 +1,1368 @@ """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.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.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.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) + added_uris = [] + removed_uris = [] + xcap_uri_map = {} + modified_uris = {} + 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 = {} + for id, changemap in list(modified_settings['uris'].modified.items()): + modified_uris[xcap_uris[id].id] = xcap_uris[id].get_modified(changemap) + xcap_uri_map[xcap_uris[id].id] = xcap_uris[id] 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) + for xcapuri_id, uri_attributes in list(modified_uris.items()): + xcap_manager.update_contact_uri(self.__xcapcontact__, xcap_uri_map[xcapuri_id], 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.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()