summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjerico.dev <jerico.dev@gmail.com>2011-06-02 03:13:11 -0300
committerRiccardo (C10uD) <c10ud.dev@gmail.com>2011-09-01 12:04:24 +0200
commit6219f54c67b2995eaacc2e88d02ab560c9b2a80c (patch)
tree000b0c50f990181b3535ca18c045b9f31076315b
parentc437c0a4d2f09ad113e8ab0a7688db1b01c9ba3b (diff)
Synchronize AB after ABCHInternal notification (closes 37962)
-rw-r--r--papyon/client.py2
-rw-r--r--papyon/event/address_book.py3
-rw-r--r--papyon/msnp/notification.py39
-rw-r--r--papyon/profile.py47
-rw-r--r--papyon/service/AddressBook/ab.py22
-rw-r--r--papyon/service/AddressBook/address_book.py265
-rw-r--r--papyon/service/AddressBook/constants.py13
-rw-r--r--papyon/service/AddressBook/scenario/contacts/__init__.py1
-rw-r--r--papyon/service/AddressBook/scenario/contacts/check_pending_invite.py37
-rw-r--r--papyon/service/AddressBook/scenario/sync/__init__.py2
-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.py28
-rw-r--r--papyon/service/description/Sharing/FindMembership.py43
-rw-r--r--papyon/util/element_tree.py29
-rw-r--r--papyon/util/iso8601/iso8601.py4
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)