diff options
author | jerico.dev <jerico.dev@gmail.com> | 2011-06-02 03:13:11 -0300 |
---|---|---|
committer | Riccardo (C10uD) <c10ud.dev@gmail.com> | 2011-09-01 12:04:24 +0200 |
commit | 6219f54c67b2995eaacc2e88d02ab560c9b2a80c (patch) | |
tree | 000b0c50f990181b3535ca18c045b9f31076315b | |
parent | c437c0a4d2f09ad113e8ab0a7688db1b01c9ba3b (diff) |
Synchronize AB after ABCHInternal notification (closes 37962)
-rw-r--r-- | papyon/client.py | 2 | ||||
-rw-r--r-- | papyon/event/address_book.py | 3 | ||||
-rw-r--r-- | papyon/msnp/notification.py | 39 | ||||
-rw-r--r-- | papyon/profile.py | 47 | ||||
-rw-r--r-- | papyon/service/AddressBook/ab.py | 22 | ||||
-rw-r--r-- | papyon/service/AddressBook/address_book.py | 265 | ||||
-rw-r--r-- | papyon/service/AddressBook/constants.py | 13 | ||||
-rw-r--r-- | papyon/service/AddressBook/scenario/contacts/__init__.py | 1 | ||||
-rw-r--r-- | papyon/service/AddressBook/scenario/contacts/check_pending_invite.py | 37 | ||||
-rw-r--r-- | papyon/service/AddressBook/scenario/sync/__init__.py | 2 | ||||
-rw-r--r-- | papyon/service/AddressBook/scenario/sync/sync.py (renamed from papyon/service/AddressBook/scenario/sync/initial_sync.py) | 13 | ||||
-rw-r--r-- | papyon/service/AddressBook/sharing.py | 28 | ||||
-rw-r--r-- | papyon/service/description/Sharing/FindMembership.py | 43 | ||||
-rw-r--r-- | papyon/util/element_tree.py | 29 | ||||
-rw-r--r-- | papyon/util/iso8601/iso8601.py | 4 |
15 files changed, 379 insertions, 169 deletions
diff --git a/papyon/client.py b/papyon/client.py index 4ccf409..c55bebe 100644 --- a/papyon/client.py +++ b/papyon/client.py @@ -276,6 +276,7 @@ class Client(EventsDispatcher): """ if (self._state != ClientState.CLOSED): logger.warning('login already in progress') + return self.__die = False self._state = ClientState.CONNECTING self._profile = profile.Profile((account, password), self._protocol) @@ -501,6 +502,7 @@ class Client(EventsDispatcher): def connect_signal(name): self.address_book.connect(name, event, name) + connect_signal("sync") connect_signal("contact-added") connect_signal("contact-pending") connect_signal("contact-deleted") diff --git a/papyon/event/address_book.py b/papyon/event/address_book.py index a7e4eba..92b7ec8 100644 --- a/papyon/event/address_book.py +++ b/papyon/event/address_book.py @@ -58,3 +58,6 @@ class AddressBookEventInterface(BaseEventInterface): def on_addressbook_group_contact_deleted(self, group, contact): pass + def on_addressbook_sync(self): + pass + diff --git a/papyon/msnp/notification.py b/papyon/msnp/notification.py index 8aa691f..813e2a7 100644 --- a/papyon/msnp/notification.py +++ b/papyon/msnp/notification.py @@ -552,14 +552,17 @@ class NotificationProtocol(BaseProtocol, Timer): # --------- Contact List ------------------------------------------------- def _handle_ADL(self, command): - if command.transaction_id == 0: # incoming ADL from the server - self._client.address_book.check_pending_invitations() if len(command.arguments) > 0 and command.arguments[0] == "OK": - if self._state != ProtocolState.OPEN: # Initial ADL + # Confirmation for one of our ADLs + if command.transaction_id != 0 \ + and self._state != ProtocolState.OPEN: + # Initial ADL self._state = ProtocolState.OPEN self._transport.enable_ping() - else: # contact Added - pass + else: + if command.payload: + # Incoming payload ADL from the server + self._client.address_book.sync(True) def _handle_RML(self, command): pass @@ -705,7 +708,31 @@ class NotificationProtocol(BaseProtocol, Timer): # --------- Notification ------------------------------------------------- def _handle_NOT(self, command): - pass + notification_xml = xml_utils.unescape(command.payload) + notification = ElementTree.fromstring(notification_xml) + + service = notification.findtext('MSG/BODY/NotificationData/Service') + if service != 'ABCHInternal': + return + + try: + notification_id = notification.attrib['id'] + site_id = notification.attrib['siteid'] + message_id = notification.find('MSG').attrib['id'] + send_device = notification.find('TO/VIA').attrib['agent'] + receiver_cid = notification.findtext('MSG/BODY/NotificationData/CID') + receiver_account = notification.find('TO').attrib['name'].lower() + + if notification_id != '0' or site_id != '45705' \ + or message_id != '0' or send_device != 'messenger' \ + or receiver_cid != str(self._client.profile.cid) \ + or receiver_account != self._client.profile.account.lower(): + return + + except KeyError: + return + + self._client.address_book.sync(True) #---------- Errors ------------------------------------------------------- def _error_handler(self, error): diff --git a/papyon/profile.py b/papyon/profile.py index 0be6a9b..6cecfae 100644 --- a/papyon/profile.py +++ b/papyon/profile.py @@ -411,9 +411,12 @@ class BaseContact(gobject.GObject): gobject.PARAM_READABLE), } - def __init__(self): + BLANK_ID = "00000000-0000-0000-0000-000000000000" + + def __init__(self, cid=None): gobject.GObject.__init__(self) + self._cid = cid or self.BLANK_ID self._client_capabilities = ClientCapabilities() self._current_media = None self._display_name = "" @@ -431,6 +434,12 @@ class BaseContact(gobject.GObject): return self._account @property + def cid(self): + """Contact ID + @rtype: GUID string""" + return self._cid + + @property def client_id(self): """The user capabilities @rtype: ClientCapabilities""" @@ -564,7 +573,7 @@ class Profile(BaseContact): self._account = account[0] self._password = account[1] - self._id = "00000000-0000-0000-0000-000000000000" + self._id = self.BLANK_ID self._profile = "" self._network_id = NetworkID.MSN self._display_name = self._account.split("@", 1)[0] @@ -767,9 +776,8 @@ class Contact(BaseContact): def __init__(self, id, network_id, account, display_name, cid=None, memberships=Membership.NONE, contact_type=ContactType.REGULAR): """Initializer""" - BaseContact.__init__(self) - self._id = id or "00000000-0000-0000-0000-000000000000" - self._cid = cid or "00000000-0000-0000-0000-000000000000" + BaseContact.__init__(self, cid) + self._id = id or self.BLANK_ID self._network_id = network_id self._account = account self._display_name = display_name @@ -805,12 +813,6 @@ class Contact(BaseContact): return self._attributes.copy() @property - def cid(self): - """Contact ID - @rtype: GUID string""" - return self._cid - - @property def groups(self): """Contact list of groups @rtype: set(L{Group<papyon.profile.Group>}...)""" @@ -859,20 +861,23 @@ class Contact(BaseContact): def is_mail_contact(self): """Determines if this contact is a mail contact""" - blank_id = "00000000-0000-0000-0000-000000000000" - return (not self.is_member(Membership.FORWARD) and self.id != blank_id) + return (not self.is_member(Membership.FORWARD) \ + and self.id != self.BLANK_ID) def _set_memberships(self, memberships): - self._memberships = memberships - self.notify("memberships") + if self._memberships != memberships: + self._memberships = memberships + self.notify("memberships") def _add_membership(self, membership): - self._memberships |= membership - self.notify("memberships") + if self._memberships != (self._memberships | membership): + self._memberships |= membership + self.notify("memberships") def _remove_membership(self, membership): - self._memberships ^= membership - self.notify("memberships") + if self._memberships != (self._memberships & ~membership): + self._memberships &= ~membership + self.notify("memberships") def _server_attribute_changed(self, name, value): self._attributes[name] = value @@ -883,8 +888,8 @@ class Contact(BaseContact): self.notify("infos") def _reset(self): - self._id = "00000000-0000-0000-0000-000000000000" - self._cid = "00000000-0000-0000-0000-000000000000" + self._id = self.BLANK_ID + self._cid = self.BLANK_ID self._groups = set() self._flags = 0 diff --git a/papyon/service/AddressBook/ab.py b/papyon/service/AddressBook/ab.py index 1b1e462..048d220 100644 --- a/papyon/service/AddressBook/ab.py +++ b/papyon/service/AddressBook/ab.py @@ -98,6 +98,12 @@ class Contact(object): for group in groups: self.Groups.append(group.text) + self.DeletedGroups = [] + deletedGroups = contact_info.find("./ab:groupIdsDeleted") + if deletedGroups is not None: + for deletedGroup in deletedGroups: + self.DeletedGroups.append(deletedGroup.text) + self.Type = contact_info.findtext("./ab:contactType") self.QuickName = contact_info.findtext("./ab:quickName") self.PassportName = contact_info.findtext("./ab:passportName") @@ -155,7 +161,7 @@ class AB(SOAPService): SOAPService.__init__(self, "AB", proxies) self._creating_ab = False - self._last_changes = DEFAULT_TIMESTAMP + self._last_changes = XMLTYPE.datetime.DEFAULT_TIMESTAMP def Add(self, callback, errback, scenario, account): """Creates the address book on the server. @@ -191,16 +197,22 @@ class AB(SOAPService): @param scenario: "Initial" | "ContactSave" ... @param deltas_only: True if the method should only check changes since last_change, otherwise False""" - if deltas_only and self._last_changes == DEFAULT_TIMESTAMP: + if self._last_changes == XMLTYPE.datetime.DEFAULT_TIMESTAMP \ + or not deltas_only: deltas_only = False + last_changes = XMLTYPE.datetime.DEFAULT_TIMESTAMP + else: + last_changes = self._last_changes self.__soap_request(callback, errback, self._service.ABFindAll, scenario, - (XMLTYPE.bool.encode(deltas_only), self._last_changes), + (XMLTYPE.bool.encode(deltas_only), + last_changes), (scenario, deltas_only)) def _HandleABFindAllResponse(self, callback, errback, response, user_data): - last_changes = response[0].find("./ab:lastChange") - if last_changes is not None: + last_changes = response[0] and response[0].find("./ab:lastChange") + if last_changes is not None \ + and XMLTYPE.datetime.decode(self._last_changes) < XMLTYPE.datetime.decode(last_changes.text): self._last_changes = last_changes.text groups = [] 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()) diff --git a/papyon/service/AddressBook/constants.py b/papyon/service/AddressBook/constants.py index 4b5ce28..43e6b8b 100644 --- a/papyon/service/AddressBook/constants.py +++ b/papyon/service/AddressBook/constants.py @@ -20,7 +20,7 @@ from papyon.errors import ClientError, ClientErrorType -__all__ = ['AddressBookError', 'AddressBookState', 'DEFAULT_TIMESTAMP'] +__all__ = ['AddressBookError', 'AddressBookState'] class AddressBookError(ClientError): @@ -119,10 +119,9 @@ class AddressBookState(object): NOT_SYNCHRONIZED = 0 """The addressbook is not synchronized yet""" - SYNCHRONIZING = 1 - """The addressbook is being synchronized""" - SYNCHRONIZED = 2 + INITIAL_SYNC = 1 + """The addressbook is being initialized""" + RESYNC = 2 + """The addressbook is being re-synchronized after an update""" + SYNCHRONIZED = 3 """The addressbook is already synchronized""" - - -DEFAULT_TIMESTAMP = "0001-01-01T00:00:00.0000000-08:00" diff --git a/papyon/service/AddressBook/scenario/contacts/__init__.py b/papyon/service/AddressBook/scenario/contacts/__init__.py index c9fc548..6fc9586 100644 --- a/papyon/service/AddressBook/scenario/contacts/__init__.py +++ b/papyon/service/AddressBook/scenario/contacts/__init__.py @@ -19,7 +19,6 @@ from accept_invite import * from decline_invite import * -from check_pending_invite import * from update_memberships import * from block_contact import * diff --git a/papyon/service/AddressBook/scenario/contacts/check_pending_invite.py b/papyon/service/AddressBook/scenario/contacts/check_pending_invite.py deleted file mode 100644 index 2402932..0000000 --- a/papyon/service/AddressBook/scenario/contacts/check_pending_invite.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007 Johann Prieur <johann.prieur@gmail.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -from papyon.service.AddressBook.scenario.base import BaseScenario -from papyon.service.AddressBook.scenario.base import Scenario - -__all__ = ['CheckPendingInviteScenario'] - -class CheckPendingInviteScenario(BaseScenario): - def __init__(self, sharing, callback, errback): - """Checks the pending invitations. - - @param sharing: the membership service - @param callback: tuple(callable, *args) - @param errback: tuple(callable, *args) - """ - BaseScenario.__init__(self, Scenario.MESSENGER_PENDING_LIST, callback, errback) - self.__sharing = sharing - - def execute(self): - self.__sharing.FindMembership(self._callback, self._errback, - self._scenario, ['Messenger'], True) diff --git a/papyon/service/AddressBook/scenario/sync/__init__.py b/papyon/service/AddressBook/scenario/sync/__init__.py index 37bc43c..05375fe 100644 --- a/papyon/service/AddressBook/scenario/sync/__init__.py +++ b/papyon/service/AddressBook/scenario/sync/__init__.py @@ -17,4 +17,4 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # -from initial_sync import * +from sync import * diff --git a/papyon/service/AddressBook/scenario/sync/initial_sync.py b/papyon/service/AddressBook/scenario/sync/sync.py index f6b940c..ffe76af 100644 --- a/papyon/service/AddressBook/scenario/sync/initial_sync.py +++ b/papyon/service/AddressBook/scenario/sync/sync.py @@ -18,10 +18,11 @@ # from papyon.service.AddressBook.scenario.base import BaseScenario -__all__ = ['InitialSyncScenario'] +__all__ = ['SyncScenario'] -class InitialSyncScenario(BaseScenario): - def __init__(self, address_book, sharing, callback, errback, account=''): +class SyncScenario(BaseScenario): + def __init__(self, address_book, sharing, callback, errback, + delta_only=False): """Synchronizes the membership content when logging in. @param address_book: the address book service @@ -36,14 +37,14 @@ class InitialSyncScenario(BaseScenario): self.__membership_response = None self.__ab_response = None - self.__account = account + self.__delta_only = delta_only def execute(self): self.__address_book.FindAll((self.__ab_findall_callback,), - self._errback, self._scenario, False) + self._errback, self._scenario, self.__delta_only) self.__sharing.FindMembership((self.__membership_findall_callback,), self._errback, self._scenario, - ['Messenger'], False) + ['Messenger'], self.__delta_only) def __membership_findall_callback(self, result): self.__membership_response = result diff --git a/papyon/service/AddressBook/sharing.py b/papyon/service/AddressBook/sharing.py index e74ad66..55f6c54 100644 --- a/papyon/service/AddressBook/sharing.py +++ b/papyon/service/AddressBook/sharing.py @@ -36,7 +36,6 @@ class Member(object): self.DisplayName = member.findtext("./ab:DisplayName") self.State = member.findtext("./ab:State") - self.Deleted = member.findtext("./ab:Deleted", "bool") self.LastChanged = member.findtext("./ab:LastChanged", "datetime") self.Changes = [] # FIXME: extract the changes self.Annotations = annotations_to_dict(member.find("./ab:Annotations")) @@ -133,7 +132,7 @@ class Sharing(SOAPService): self._tokens = {} SOAPService.__init__(self, "Sharing", proxies) - self._last_changes = "0001-01-01T00:00:00.0000000-08:00" + self._last_changes = XMLTYPE.datetime.DEFAULT_TIMESTAMP def FindMembership(self, callback, errback, scenario, services, deltas_only): """Requests the membership list. @@ -147,25 +146,36 @@ class Sharing(SOAPService): @param deltas_only: True if the method should only check changes since last_change, False else """ + if self._last_changes == XMLTYPE.datetime.DEFAULT_TIMESTAMP \ + or not deltas_only: + deltas_only = False + last_changes = XMLTYPE.datetime.DEFAULT_TIMESTAMP + else: + last_changes = self._last_changes self.__soap_request(callback, errback, self._service.FindMembership, scenario, - (services, deltas_only, self._last_changes), + (services, + XMLTYPE.bool.encode(deltas_only), + last_changes), (scenario, services)) def _HandleFindMembershipResponse(self, callback, errback, response, user_data): - if response[1] is not None: - self._last_changes = response[1] - memberships = {} + last_changes = response[1] + if last_changes == "" \ + or XMLTYPE.datetime.decode(self._last_changes) < XMLTYPE.datetime.decode(last_changes): + if last_changes != "": + self._last_changes = last_changes + for role, members in response[0].iteritems(): for member in members: - membership_id = XMLTYPE.int.decode(member.find("./ab:MembershipId").text) + deleted = member.findtext("./ab:Deleted", "bool") member_obj = Member.new(member) member_id = hash(member_obj) if member_id in memberships: - memberships[member_id].Roles[role] = membership_id + memberships[member_id].Roles[role] = deleted else: - member_obj.Roles[role] = membership_id + member_obj.Roles[role] = deleted memberships[member_id] = member_obj run(callback, memberships.values()) diff --git a/papyon/service/description/Sharing/FindMembership.py b/papyon/service/description/Sharing/FindMembership.py index 5c6020a..b5ecfc3 100644 --- a/papyon/service/description/Sharing/FindMembership.py +++ b/papyon/service/description/Sharing/FindMembership.py @@ -18,7 +18,6 @@ # from common import * -from papyon.profile import Membership import xml.sax.saxutils as xml @@ -43,27 +42,23 @@ def soap_body(services_types, deltas_only, last_change): %s </ServiceType>""" % xml.escape(service) - deltas = '' - if deltas_only: - deltas = """<View xmlns="http://www.msn.com/webservices/AddressBook"> - Full - </View> - <deltasOnly xmlns="http://www.msn.com/webservices/AddressBook"> - true - </deltasOnly> - <lastChange xmlns="http://www.msn.com/webservices/AddressBook"> - %s - </lastChange>""" % last_change - return """ - <FindMembership xmlns="http://www.msn.com/webservices/AddressBook"> - <serviceFilter xmlns="http://www.msn.com/webservices/AddressBook"> - <Types xmlns="http://www.msn.com/webservices/AddressBook"> - %(services)s - </Types> - </serviceFilter> - %(deltas)s - </FindMembership>""" % {'services' : services, 'deltas' : deltas} + <FindMembership xmlns="http://www.msn.com/webservices/AddressBook"> + <serviceFilter xmlns="http://www.msn.com/webservices/AddressBook"> + <Types xmlns="http://www.msn.com/webservices/AddressBook"> + %(services)s + </Types> + </serviceFilter> + <View xmlns="http://www.msn.com/webservices/AddressBook"> + Full + </View> + <deltasOnly xmlns="http://www.msn.com/webservices/AddressBook"> + %(delta_only)s + </deltasOnly> + <lastChange xmlns="http://www.msn.com/webservices/AddressBook"> + %(last_change)s + </lastChange> + </FindMembership>""" % {'services' : services, 'delta_only' : deltas_only, 'last_change': last_change} def process_response(soap_response): # FIXME: don't pick the 1st service only, we need to extract them all @@ -80,7 +75,7 @@ def process_response(soap_response): if role is None or len(members) == 0: continue result[role.text] = members - last_changes = service.findtext("./ab:LastChange") + last_change = service.findtext("./ab:LastChange") else: - last_changes = "0001-01-01T00:00:00.0000000-08:00" - return (result, last_changes) + last_change = None + return (result, last_change) diff --git a/papyon/util/element_tree.py b/papyon/util/element_tree.py index b026ffb..59d1b4a 100644 --- a/papyon/util/element_tree.py +++ b/papyon/util/element_tree.py @@ -61,14 +61,36 @@ class XMLTYPE(object): return 0 class datetime(object): + DEFAULT_TIMESTAMP = "0001-01-01T00:00:00.0000000-08:00" + @staticmethod def encode(datetime): return datetime.isoformat() @staticmethod def decode(date_str): + """Examples: + >>> XMLTYPE.datetime.decode('2011-05-13T17:45:23.0123456') + datetime.datetime(2011, 5, 13, 17, 45, 23, 12345) + >>> XMLTYPE.datetime.decode('2011-05-13T14:45:23.321-03:00') + datetime.datetime(2011, 5, 13, 17, 45, 23, 321000) + >>> XMLTYPE.datetime.decode('2011-05-13T17:45:23.12345678Z') + datetime.datetime(2011, 5, 13, 17, 45, 23, 123456) + >>> XMLTYPE.datetime.decode('2011-05-13T17:45:23') + datetime.datetime(2011, 5, 13, 17, 45, 23) + >>> XMLTYPE.datetime.decode('2011-05-13T17:45:23Z') + datetime.datetime(2011, 5, 13, 17, 45, 23) + >>> XMLTYPE.datetime.decode('2011-05-13T14:45:23-03:00') + datetime.datetime(2011, 5, 13, 17, 45, 23) + >>> XMLTYPE.datetime.decode('') + datetime.datetime(1, 1, 1, 8, 0) + >>> XMLTYPE.datetime.decode(None) + datetime.datetime(1, 1, 1, 8, 0) + """ + if date_str is None or date_str == '': + date_str = XMLTYPE.datetime.DEFAULT_TIMESTAMP result = iso8601.parse_date(date_str.strip()) - return result.replace(tzinfo=None) # FIXME: do not disable the timezone + return result.replace(tzinfo=None) - result.utcoffset() class _Element(object): def __init__(self, element, ns_shorthands): @@ -162,3 +184,8 @@ class XMLResponse(object): def _parse(self, data): pass + + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/papyon/util/iso8601/iso8601.py b/papyon/util/iso8601/iso8601.py index 68ed05a..dec97d4 100644 --- a/papyon/util/iso8601/iso8601.py +++ b/papyon/util/iso8601/iso8601.py @@ -94,9 +94,9 @@ def parse_date(datestring, default_timezone=UTC): groups = m.groupdict() tz = parse_timezone(groups["timezone"], default_timezone=UTC) if groups["fraction"] is None: - groups["fraction"] = 0 + groups["fraction"] = "0" frac = int(groups["fraction"]) - groups["fraction"] = int (frac / 10 ** (len(str(frac)) - 6)) + groups["fraction"] = int (frac / 10 ** (len(groups["fraction"]) - 6)) return datetime(int(groups["year"]), int(groups["month"]), int(groups["day"]), int(groups["hour"]), int(groups["minute"]), int(groups["second"]), int(groups["fraction"]), tz) |