diff options
Diffstat (limited to 'papyon/service/AddressBook/address_book.py')
-rw-r--r-- | papyon/service/AddressBook/address_book.py | 265 |
1 files changed, 216 insertions, 49 deletions
diff --git a/papyon/service/AddressBook/address_book.py b/papyon/service/AddressBook/address_book.py index b2ac937..43e3b33 100644 --- a/papyon/service/AddressBook/address_book.py +++ b/papyon/service/AddressBook/address_book.py @@ -25,7 +25,7 @@ import scenario import papyon import papyon.profile as profile -from papyon.profile import Membership, NetworkID +from papyon.profile import Membership, NetworkID, Contact from papyon.util.decorator import rw_property from papyon.profile import ContactType from papyon.service.AddressBook.constants import * @@ -35,6 +35,10 @@ from papyon.util.async import run import gobject +import logging +logger = logging.getLogger('papyon.service.address_book') + + __all__ = ['AddressBook', 'AddressBookState'] class AddressBookStorage(set): @@ -133,6 +137,10 @@ class AddressBook(gobject.GObject): gobject.TYPE_NONE, (object,)), + "sync" : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ()), + "contact-added" : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object,)), @@ -198,6 +206,8 @@ class AddressBook(gobject.GObject): def __init__(self, sso, client, proxies=None): """The address book object.""" gobject.GObject.__init__(self) + self.__frozen = 0 + self.__signal_queue = [] self._ab = ab.AB(sso, client, proxies) self._sharing = sharing.Sharing(sso, proxies) @@ -209,6 +219,8 @@ class AddressBook(gobject.GObject): self.contacts = AddressBookStorage() self._profile = None + self.connect_after('contact-deleted', lambda self, contact: contact._reset()) + # Properties @property def state(self): @@ -227,31 +239,31 @@ class AddressBook(gobject.GObject): def profile(self): return self._profile - def sync(self): - if self._state != AddressBookState.NOT_SYNCHRONIZED: + def sync(self, delta_only=False): + # Avoid race conditions. + if self._state in \ + (AddressBookState.INITIAL_SYNC, AddressBookState.RESYNC): return - self._state = AddressBookState.SYNCHRONIZING - def callback(address_book, memberships): - for group in address_book.groups: - g = profile.Group(group.Id, group.Name.encode("utf-8")) - self.groups.add(g) - for contact in address_book.contacts: - c = self.__build_contact(contact, Membership.FORWARD) - if c is None: - continue - if contact.Type == ContactType.ME: - self._profile = c - else: - self.contacts.add(c) + if self._state == AddressBookState.NOT_SYNCHRONIZED: + self._state = AddressBookState.INITIAL_SYNC + else: + self._state = AddressBookState.RESYNC + + def callback(ab_storage, memberships): + self.__log_sync_request(ab_storage, memberships) + self.__freeze_address_book() + self.__update_address_book(ab_storage) self.__update_memberships(memberships) + self.__unfreeze_address_book() self._state = AddressBookState.SYNCHRONIZED + self.__common_callback('sync', done_cb) - initial_sync = scenario.InitialSyncScenario(self._ab, self._sharing, + sc = scenario.SyncScenario(self._ab, self._sharing, (callback,), (self.__common_errback,), - self._client.profile.account) - initial_sync() + delta_only) + sc() # Public API def search_contact(self, account, network_id): @@ -273,16 +285,6 @@ class AddressBook(gobject.GObject): contact = profile.Contact(None, network_id, account, display_name) return contact - def check_pending_invitations(self, done_cb=None, failed_cb=None): - def callback(memberships): - self.__update_memberships(memberships) - self.__common_callback('contact-pending', done_cb, - self.contacts.search_by_memberships(Membership.PENDING)) - cp = scenario.CheckPendingInviteScenario(self._sharing, - (callback,), - (self.__common_errback, failed_cb)) - cp() - def accept_contact_invitation(self, pending_contact, add_to_contact_list=True, done_cb=None, failed_cb=None): def callback(contact_infos, memberships): @@ -334,10 +336,8 @@ class AddressBook(gobject.GObject): scenario_class = MessengerContactAddScenario s = scenario_class(self._ab, (callback,), - (self.__common_errback, failed_cb)) - s.account = account - s.network_id = network_id - s.memberships = old_memberships + (self.__common_errback, failed_cb), + account, network_id, old_memberships) s.auto_manage_allow_list = auto_allow s.invite_display_name = invite_display_name s.invite_message = invite_message @@ -345,11 +345,12 @@ class AddressBook(gobject.GObject): def upgrade_mail_contact(self, contact, groups=[], done_cb=None, failed_cb=None): + logger.info('upgrade mail contact: %s' % str(contact)) def callback(): contact._add_membership(Membership.ALLOW) for group in groups: self.add_contact_to_group(group, contact) - self.__common_callback(None, done_cb) + self.__common_callback(None, done_cb, contact) up = scenario.ContactUpdatePropertiesScenario(self._ab, (callback,), (self.__common_errback, failed_cb)) @@ -360,11 +361,7 @@ class AddressBook(gobject.GObject): def delete_contact(self, contact, done_cb=None, failed_cb=None): def callback(): - contact._remove_membership(Membership.FORWARD) - self.__common_callback('contact-deleted', done_cb, contact) - contact._reset() - if contact.memberships == Membership.NONE: - self.contacts.discard(contact) + self.__remove_contact(contact, Membership.FORWARD, done_cb) dc = scenario.ContactDeleteScenario(self._ab, (callback,), @@ -455,10 +452,7 @@ class AddressBook(gobject.GObject): def delete_group(self, group, done_cb=None, failed_cb=None): def callback(): - for contact in self.contacts: - contact._delete_group_ownership(group) - self.groups.discard(group) - self.__common_callback('group-deleted', done_cb, group) + self.__remove_group(group, done_cb) dg = scenario.GroupDeleteScenario(self._ab, (callback,), (self.__common_errback, failed_cb)) @@ -498,8 +492,39 @@ class AddressBook(gobject.GObject): dc.group_guid = group.id dc.contact_guid = contact.id dc() + + def emit(self, detailed_signal, *args, **kwargs): + if self.__frozen: + self.__signal_queue.append((detailed_signal, args, kwargs)) + else: + super(AddressBook, self).emit(detailed_signal, *args, **kwargs) + # End of public API + def __freeze_address_book(self): + """Disable all AB notifications and events until we unfreeze.""" + if not self.__frozen: + self.freeze_notify() + for group in self.groups: + group.freeze_notify() + for contact in self.contacts: + contact.freeze_notify() + self.__frozen += 1 + + def __unfreeze_address_book(self): + """Emit all queued AB notifications and events.""" + if self.__frozen: + self.__frozen -= 1 + if not self.__frozen: + for contact in self.contacts: + contact.thaw_notify() + for group in self.groups: + group.thaw_notify() + self.thaw_notify() + for signal in self.__signal_queue: + super(AddressBook, self).emit(signal[0], *signal[1], **signal[2]) + self.__signal_queue = [] + def __build_contact(self, contact=None, memberships=Membership.NONE): external_email = None is_messenger_enabled = False @@ -568,11 +593,14 @@ class AddressBook(gobject.GObject): if infos is not None: contact._id = infos.Id contact._cid = infos.CID - contact._display_name = infos.DisplayName + if infos.DisplayName: + contact._display_name = infos.DisplayName contact._server_infos_changed(infos.contact_infos) for group in self.groups: if group.id in infos.Groups: contact._add_group_ownership(group) + if group.id in infos.DeletedGroups: + contact._delete_group_ownership(group) contact.thaw_notify() def __build_or_update_contact(self, account, network_id=NetworkID.MSN, @@ -591,6 +619,109 @@ class AddressBook(gobject.GObject): self.emit('contact-added', contact) return contact + def __remove_contact(self, contact, removed_memberships, done_cb=None): + emit_deleted = False + if removed_memberships & Membership.FORWARD \ + and contact.is_member(Membership.FORWARD): + emit_deleted = True + removed_memberships |= Membership.REVERSE + contact._remove_membership(removed_memberships) + # Do not use __common_callback() here to avoid race + # conditions with the event-triggered contact._reset(). + run(done_cb, contact) + if contact.memberships == Membership.NONE: + self.contacts.discard(contact) + if emit_deleted: + self.emit('contact-deleted', contact) + + def __remove_group(self, group, done_cb=None): + for contact in self.contacts: + contact._delete_group_ownership(group) + self.groups.discard(group) + self.__common_callback('group-deleted', done_cb, group) + + def __log_sync_request(self, ab_storage, memberships): + myself = '???' if not self._profile else self._profile.account + contacts = ['%s-%s' % ('D' if contact.Deleted else 'A', + contact.PassportName) + for contact in ab_storage.contacts] + groups = ['%s-%s' % ('D' if group.Deleted else 'A', + group.Name) + for group in ab_storage.groups] + members = [] + for member in memberships: + member_repr = member.PassportName + for role, deleted in member.Roles.items(): + member_repr += ' %s-%s' % ('D' if deleted else 'A', role) + members.append(member_repr) + logger.info('[%s] Received sync request:\n' + '...contacts:\n' + '%s\n' + '...groups:\n' + '%s\n' + '...memberships:\n' + '%s' + % (myself, str(contacts), str(groups), str(members))) + + def __update_address_book(self, ab_storage): + for group_infos in ab_storage.groups: + group = None + for g in self.groups: + if g.id == group_infos.Id: + group = g + break + + if group_infos.Deleted: + if group is not None: + self.__remove_group(group) + else: + group_name = group_infos.Name.encode("utf-8") + if group: + group._server_property_changed('name', group_name) + else: + group = profile.Group(group_infos.Id, group_name) + group.freeze_notify() + self.groups.add(group) + if self.state != AddressBookState.INITIAL_SYNC: + self.emit('group-added', group) + + for contact_infos in ab_storage.contacts: + new_contact = self.__build_contact(contact_infos, Membership.FORWARD) + if new_contact is None: + continue + new_contact.freeze_notify() + + contact = self.search_contact(new_contact.account, + new_contact.network_id) + + if contact_infos.Type == ContactType.ME: + if self._profile is None: + self._profile = new_contact + else: + self.__update_contact(self._profile, infos=contact_infos) + continue + + if contact_infos.Deleted: + if contact: + self.__remove_contact(contact, Membership.FORWARD) + else: + new_contact_added = False + if not contact \ + or contact.id == Contact.BLANK_ID: + new_contact_added = True + + if contact: + self.__update_contact(contact, infos=contact_infos) + else: + contact = new_contact + self.contacts.add(contact) + + if new_contact_added: + if not contact.is_member(Membership.PENDING): + contact._add_membership(Membership.FORWARD) + if self.state != AddressBookState.INITIAL_SYNC: + self.emit('contact-added', contact) + def __update_memberships(self, members): role_to_membership = { "Allow" : Membership.ALLOW, @@ -599,6 +730,13 @@ class AddressBook(gobject.GObject): "Pending" : Membership.PENDING } + membership_conflicts = { + Membership.ALLOW: Membership.BLOCK, + Membership.BLOCK: Membership.ALLOW, + Membership.FORWARD: Membership.PENDING, + Membership.PENDING: Membership.FORWARD + } + for member in members: if isinstance(member, sharing.PassportMember): network = NetworkID.MSN @@ -611,28 +749,55 @@ class AddressBook(gobject.GObject): continue # ignore contacts with hidden passport name contact = self.search_contact(member.Account, network) + new_contact = False if contact is None: + member_deleted = True + for role, deleted in member.Roles.items(): + if not deleted: + member_deleted = False + break + if member_deleted: + continue + new_contact = True cid = getattr(member, "CID", None) account = member.Account.encode("utf-8") display_name = (member.DisplayName or member.Account).encode("utf-8") msg = member.Annotations.get('MSN.IM.InviteMessage', u'') contact = profile.Contact(None, network, account, display_name, cid) + contact.freeze_notify() contact._server_attribute_changed('invite_message', msg.encode("utf-8")) self.contacts.add(contact) if contact is self._client.profile: continue # don't update our own memberships - for role in member.Roles: + # TODO: Check whether the contact's membership was changed + # after member.LastChanged and if so ignore this member. + # To implement this papyon has to save full membership info + # for contacts. + + deleted_memberships = Membership.NONE + for role, deleted in member.Roles.items(): membership = role_to_membership.get(role, None) if membership is None: raise NotImplementedError("Unknown Membership:" + membership) - contact._add_membership(membership) - if new_contact and self.state == AddressBookState.SYNCHRONIZED: - self.emit('contact-added', contact) + if deleted: + deleted_memberships |= membership + else: + conflicting_memberships = membership_conflicts.get(membership, Membership.NONE) + contact._remove_membership(conflicting_memberships) + contact._add_membership(membership) + + if deleted_memberships: + self.__remove_contact(contact, deleted_memberships) + if self.state != AddressBookState.INITIAL_SYNC: + if contact.is_member(Membership.PENDING): + self.emit('contact-pending', contact) + if new_contact: + self.emit('contact-added', contact) # Callbacks def __common_callback(self, signal, callback, *args): @@ -642,6 +807,8 @@ class AddressBook(gobject.GObject): def __common_errback(self, error, errback=None): run(errback, error) + while self.__frozen: + self.__unfreeze_address_book() self.emit('error', error) gobject.type_register(AddressBook) @@ -701,7 +868,7 @@ if __name__ == '__main__': print address_book.contacts[0].account address_book.update_contact_infos(address_book.contacts[0], {ContactGeneral.FIRST_NAME : "lolibouep"}) - #address_book._check_pending_invitations() + #address_book.sync(True) #address_book.accept_contact_invitation(address_book.pending_contacts.pop()) #print address_book.pending_contacts.pop() #address_book.accept_contact_invitation(address_book.pending_contacts.pop()) |