[Telepathy-commits] [telepathy-pinocchio/master] save contact list changes to disk each time it changes

Travis Reitter travis.reitter at collabora.co.uk
Thu Aug 14 17:07:59 PDT 2008


---
 bin/pinocchio-ctl                 |   11 +----
 pinocchio/channel/contact_list.py |    8 +++
 pinocchio/common.py               |  106 ++++++++++++++++++++++++++++--------
 pinocchio/connection/__init__.py  |   77 +++++++++++++++++++++++++--
 pinocchio/connection/aliasing.py  |    1 +
 pinocchio/connection/avatars.py   |    1 +
 pinocchio/connection/presence.py  |    1 +
 pinocchio/server/__init__.py      |   30 ++++++++++
 8 files changed, 197 insertions(+), 38 deletions(-)

diff --git a/bin/pinocchio-ctl b/bin/pinocchio-ctl
index 7e8b679..26a5dd9 100755
--- a/bin/pinocchio-ctl
+++ b/bin/pinocchio-ctl
@@ -877,8 +877,7 @@ def validate_args(argv):
     if len(sys.argv) <= 1:
         print_help_exit(1)
 
-    command_opts = {'contacts-file': None,
-                    'username': pin.common.ACCOUNT_DEFAULT,
+    command_opts = {'username': pin.common.ACCOUNT_DEFAULT,
                     'password': ''}
     try:
         opts, args = getopt.getopt(sys.argv[1:], "p:u:",
@@ -895,15 +894,7 @@ def validate_args(argv):
             print_help_exit(3, pre_message=str('unsupported option'))
 
     ### validate argument values ###
-
     account_id = tp.server.conn._escape_as_identifier(command_opts['username'])
-    # ensure this is an absolute path
-    command_opts['contacts-file'] = pin.common.get_contacts_file_abs(
-                                                command_opts['contacts-file'],
-                                                account_id=account_id)
-    if not os.path.isfile(command_opts['contacts-file']):
-        print_help_exit(4, pre_message=str('file does not exist: %s' % \
-                                                command_opts['contacts-file']))
 
     command = args[0]
     command_args = args[1:]
diff --git a/pinocchio/channel/contact_list.py b/pinocchio/channel/contact_list.py
index bda8b52..ba620ec 100644
--- a/pinocchio/channel/contact_list.py
+++ b/pinocchio/channel/contact_list.py
@@ -83,6 +83,8 @@ class ContactList(tp.server.ChannelTypeContactList,
                             self._connection._self_handle,
                             tp.constants.CHANNEL_GROUP_CHANGE_REASON_NONE)
 
+        self._connection.save()
+
     def RemoveMembers(self, contacts, message):
         """Remove list of contacts from this group.
         
@@ -105,6 +107,8 @@ class ContactList(tp.server.ChannelTypeContactList,
                             self._connection._self_handle,
                             tp.constants.CHANNEL_GROUP_CHANGE_REASON_NONE)
 
+        self._connection.save()
+
 class Group(tp.server.ChannelTypeContactList, tp.server.ChannelInterfaceGroup):
     """
     A user-defined group of contacts.
@@ -146,6 +150,8 @@ class Group(tp.server.ChannelTypeContactList, tp.server.ChannelInterfaceGroup):
                             self._connection._self_handle,
                             tp.constants.CHANNEL_GROUP_CHANGE_REASON_NONE)
 
+        self._connection.save()
+
     def RemoveMembers(self, contacts, message):
         """Remove list of contacts from this group.
         
@@ -167,3 +173,5 @@ class Group(tp.server.ChannelTypeContactList, tp.server.ChannelInterfaceGroup):
         self.MembersChanged(message, (), handle_objs, (), (),
                             self._connection._self_handle,
                             tp.constants.CHANNEL_GROUP_CHANGE_REASON_NONE)
+
+        self._connection.save()
diff --git a/pinocchio/common.py b/pinocchio/common.py
index a2f20b4..f78536f 100644
--- a/pinocchio/common.py
+++ b/pinocchio/common.py
@@ -25,52 +25,80 @@ import telepathy as tp
 PROTO_DEFAULT = 'dummy'
 ACCOUNT_DEFAULT = 'default at default'
 
-CONTACTS_FILE_DEFAULT='contacts.xml'
-CONTACTS_FILE_MODIFIED='contacts-modified.xml'
-# XXX: is there a better place to default to?
+CONTACTS_FILENAME='contacts.xml'
+# this is a pseudo-enum
+PREFIX_DEFAULT, PREFIX_SAVED, PREFIX_SAVED_PREFERRED = range(3)
 DATA_DIR_DEFAULT = '/var/lib/telepathy-pinocchio'
+DATA_DIR_SAVED = os.path.join(os.path.expanduser('~'), '.telepathy-pinocchio')
 AVATAR_DIR_DEFAULT = os.path.join(DATA_DIR_DEFAULT, 'avatars')
+AVATAR_DIR_SAVED = os.path.join(DATA_DIR_SAVED, 'avatars')
 
 CM_PINOCCHIO = '.'.join((tp.interfaces.CONNECTION_MANAGER, 'pinocchio'))
 CM_PINOCCHIO_OBJ = '/' + CM_PINOCCHIO.replace('.', '/')
 
-def get_account_dir(account_id=None):
-    if not account_id:
-        account_id = tp.server.conn._escape_as_identifier(ACCOUNT_DEFAULT)
-    return os.path.join(DATA_DIR_DEFAULT, 'accounts', account_id)
+def get_account_dir(prefix, account_id):
+    """Get the base account directory for default or saved contacts file, etc.
+
+    Arguments:
+    prefix -- PREFIX_DEFAULT to get the unmodified contacts file
+              PREFIX_SAVED to get the saved contacts file
+    account_id -- escaped account name
+
+    Exceptions:
+    ValueError -- invalid account_id or prefix
+    """
+    dir_result = None
 
-def get_contacts_file_abs(contacts_file=None, account_id=None):
+    if   prefix == PREFIX_DEFAULT:
+        dir_result = os.path.join(DATA_DIR_DEFAULT, 'accounts', account_id)
+    elif prefix == PREFIX_SAVED:
+        dir_result = os.path.join(DATA_DIR_SAVED, 'accounts', account_id)
+    else:
+        raise ValueError, 'invalid contacts file argument'
+
+    return dir_result
+
+def get_contacts_file_abs(account_id, prefix=PREFIX_SAVED_PREFERRED):
     """Returns the absolute path for the contacts file (prepending the account's
     base data dir as necessary). Modified rosters are saved to disk as a
     different name, so this function prefers them over the default file name.
+    Note that this method does not check that the final result or its parent
+    directories exist, so the caller is still responsible to confirm they exist
+    and create them as necessary.
 
     Arguments:
-    contacts_file -- CONTACTS_FILE_DEFAULT to get the unmodified contacts file
-                     CONTACTS_FILE_MODIFIED to get the modified contacst file
-                     None to get the modified file if it exists, else default
-    account -- raw name of the account (default: ACCOUNT_DEFAULT)
+    account_id -- escaped account name
+    prefix -- PREFIX_DEFAULT to get the unmodified contacts file
+              PREFIX_SAVED to get the saved contacts file
+              PREFIX_SAVED_PREFERRED to get the saved file if it exists, else
+                  default
 
     Returns:
     contacts_file -- absolute path for the contacts file
+
+    Exceptions:
+    ValueError -- invalid account_id or prefix
     """
     # get the sanitized name of the default account if one was not given
     if not account_id:
-        account_id = tp.server.conn._escape_as_identifier(ACCOUNT_DEFAULT)
+        raise ValueError, 'an account ID must be provided'
 
-    account_dir = get_account_dir(account_id)
+    account_dir_default = get_account_dir(PREFIX_DEFAULT, account_id)
+    account_dir_saved = get_account_dir(PREFIX_SAVED, account_id)
+    contacts_file_default = os.path.join(account_dir_default, CONTACTS_FILENAME)
+    contacts_file_saved = os.path.join(account_dir_saved, CONTACTS_FILENAME)
 
     contacts_file_final = None
 
-    if   contacts_file in (CONTACTS_FILE_DEFAULT, CONTACTS_FILE_MODIFIED):
-        contacts_file_final = os.path.join(account_dir, CONTACTS_FILE_MODIFIED)
-    elif contacts_file == None:
-        # if contact list has been modified, it will be saved with this name
-        contacts_file_final = os.path.join(account_dir, CONTACTS_FILE_MODIFIED)
-
-        # otherwise, use the default file path
-        if not os.path.isfile(contacts_file_final):
-            contacts_file_final = os.path.join(account_dir,
-                                               CONTACTS_FILE_DEFAULT)
+    if   prefix == PREFIX_DEFAULT:
+        contacts_file_final = contacts_file_default
+    elif prefix == PREFIX_SAVED:
+        contacts_file_final = contacts_file_saved
+    elif prefix == PREFIX_SAVED_PREFERRED:
+        if os.path.isfile(contacts_file_saved):
+            contacts_file_final = contacts_file_saved
+        else:
+            contacts_file_final = contacts_file_default
     else:
         raise ValueError, 'invalid contacts file argument'
 
@@ -98,3 +126,33 @@ def image_filename_to_mime_type(file_path):
                                       '(based on its name)')
 
     return mime_type
+
+def xml_insert_element(xml_doc, parent_node, element_name, element_content,
+                       trailer='\n'):
+    """Create and fill a new element, and insert it at the end of a parent node.
+    This method also inserts a newline after the new element for human
+    readability.
+
+    Arguments:
+    xml_doc -- minidom Document object of the encompassing document
+    parent_node -- minidom Node object
+    element_name -- new element's name
+    element_contact -- new element's content
+    trailer -- text to insert after the new element's end tag (default: '\n')
+
+    Returns:
+    element -- the newly-created element Node object
+    """
+    element = xml_doc.createElement(element_name)
+
+    element_node = xml_doc.createTextNode(element_content)
+    element.appendChild(element_node)
+
+    parent_node.appendChild(element)
+    
+    # add newline for human-readability
+    if trailer:
+        trailer_node = xml_doc.createTextNode(trailer)
+        parent_node.appendChild(trailer_node)
+
+    return element
diff --git a/pinocchio/connection/__init__.py b/pinocchio/connection/__init__.py
index ae91dfa..0c2ff26 100644
--- a/pinocchio/connection/__init__.py
+++ b/pinocchio/connection/__init__.py
@@ -18,6 +18,7 @@
 # 02110-1301 USA
 
 import os.path
+from xml.dom import minidom
 
 import telepathy as tp
 
@@ -61,7 +62,8 @@ class Connection(tp.server.Connection,
 
         self._manager = manager
         self._contacts_file = pin.common.get_contacts_file_abs(
-                                                        account_id=account_id)
+                                            account_id,
+                                            pin.common.PREFIX_SAVED_PREFERRED)
 
     def Connect(self):
         """Request connection establishment."""
@@ -182,9 +184,13 @@ class Connection(tp.server.Connection,
         """
         self.Disconnect()
 
-        # TODO: delete any modified version of the contact list, once we've
-        # implemented that (the default contact list will always remain
-        # unmodified)
+        # clear out any modified version of the contact list (upon next
+        # Connect(), the default contact list will be read in)
+        account_id = self.get_account_id()
+        filename = pin.common.get_contacts_file_abs(account_id,
+                                                    pin.common.PREFIX_SAVED)
+        if os.path.isfile(filename):
+            os.remove(filename)
 
     def _channel_get_or_create (self, channel_type, handle_type, handle,
                                 suppress_handler):
@@ -287,3 +293,66 @@ class Connection(tp.server.Connection,
         """Returns the sanitized account name for the given connection."""
 
         return self._name.get_name().split('.')[-1]
+
+    def get_contact_channel_membership_info(self):
+        """Returns a map of contacts to their contact lists and groups.
+
+        Returns:
+        mapping -- dict of handle IDs to [contact list names, group names]
+        """
+        MAPPING_CONTACT_LISTS = 0
+        MAPPING_GROUPS = 1
+        mapping = {}
+
+        for channel in self._channels:
+            if channel.GetChannelType() == \
+                                        tp.interfaces.CHANNEL_TYPE_CONTACT_LIST:
+                channel_handle_type, ignore = channel.GetHandle()
+
+                if   channel_handle_type == tp.constants.HANDLE_TYPE_LIST:
+                    mapping_pos = MAPPING_CONTACT_LISTS
+                elif channel_handle_type == tp.constants.HANDLE_TYPE_GROUP:
+                    mapping_pos = MAPPING_GROUPS
+
+                # TODO: also factor in local_pending and remote_pending
+                members = channel.GetMembers()
+
+                for member_id in members:
+                    # make space for the lists if we don't already have it
+                    if member_id not in mapping:
+                        mapping[member_id] = [[], []]
+
+                    channel_name = channel._handle.get_name()
+                    mapping[member_id][mapping_pos].append(channel_name)
+
+        return mapping
+
+    def save(self):
+        """Writes the current contact list, group, and contact state out to a
+        new contacts file.
+        """
+        dom_impl = minidom.getDOMImplementation()
+        xml_doc = dom_impl.createDocument(None, 'roster', None)
+        roster_xml = xml_doc.documentElement
+
+        # add newline for human-readability
+        newline_value = xml_doc.createTextNode('\n')
+        roster_xml.appendChild(newline_value)
+
+        contact_channels_map = self.get_contact_channel_membership_info()
+        for handle_obj, lists_groups in contact_channels_map.items():
+            contact_lists, groups = lists_groups
+
+            contact_xml = handle_obj.get_xml(contact_lists, groups)
+            roster_xml.appendChild(contact_xml)
+
+            # add newline for human-readability
+            newline_value = xml_doc.createTextNode('\n\n')
+            roster_xml.appendChild(newline_value)
+
+        account_id = self.get_account_id()
+        filename = pin.common.get_contacts_file_abs(account_id,
+                                                    pin.common.PREFIX_SAVED)
+        file = open(filename, 'w+')
+        file.write(xml_doc.toxml(encoding='utf-8'))
+        file.close()
diff --git a/pinocchio/connection/aliasing.py b/pinocchio/connection/aliasing.py
index 3691485..c0a3d7a 100644
--- a/pinocchio/connection/aliasing.py
+++ b/pinocchio/connection/aliasing.py
@@ -98,3 +98,4 @@ class Aliasing(tp.server.ConnectionInterfaceAliasing):
                                        alias_map[handle_id]))
         if len(alias_pairs) >= 1:
             self.AliasesChanged(alias_pairs)
+            self.save()
diff --git a/pinocchio/connection/avatars.py b/pinocchio/connection/avatars.py
index 0ebdb29..3defcf6 100644
--- a/pinocchio/connection/avatars.py
+++ b/pinocchio/connection/avatars.py
@@ -140,3 +140,4 @@ class Avatars(tp.server.ConnectionInterfaceAvatars):
         pin.common.image_filename_to_mime_type(avatar_path)
 
         handle_obj.set_avatar(avatar_path)
+        self.save()
diff --git a/pinocchio/connection/presence.py b/pinocchio/connection/presence.py
index 3034252..38b710c 100644
--- a/pinocchio/connection/presence.py
+++ b/pinocchio/connection/presence.py
@@ -146,3 +146,4 @@ class Presence(tp.server.ConnectionInterfacePresence):
                     ids_matched.append(handle_obj.get_id())
         if len(ids_matched) >= 1:
             self.RequestPresence(ids_matched)
+            self.save()
diff --git a/pinocchio/server/__init__.py b/pinocchio/server/__init__.py
index e1205a7..f7b473e 100644
--- a/pinocchio/server/__init__.py
+++ b/pinocchio/server/__init__.py
@@ -152,6 +152,36 @@ class HandleContact(tp.server.Handle):
     def set_status_message(self, status_message):
         self._extended_attrs['status_message'] = status_message
 
+    def get_xml(self, contact_lists, groups):
+        dom_impl = minidom.getDOMImplementation()
+        xml_doc = dom_impl.createDocument(None, 'contact', None)
+        contact_xml = xml_doc.documentElement
+
+        # add newline for human-readability
+        newline_value = xml_doc.createTextNode('\n')
+        contact_xml.appendChild(newline_value)
+
+        pin.common.xml_insert_element(xml_doc, contact_xml, 'username',
+                                      self.get_name())
+
+        skip_attrs = ('avatar_bin', 'avatar_mime', 'avatar_token')
+        for attr, value in self._extended_attrs.items():
+            if attr not in skip_attrs:
+                pin.common.xml_insert_element(xml_doc, contact_xml, attr, value)
+
+        contact_lists_xml = pin.common.xml_insert_element(xml_doc, contact_xml,
+                                                          'contact_lists', '\n')
+        for contact_list in contact_lists:
+            pin.common.xml_insert_element(xml_doc, contact_lists_xml, 'list',
+                                          contact_list)
+
+        groups_xml = pin.common.xml_insert_element(xml_doc, contact_xml,
+                                                   'groups', '\n')
+        for group in groups:
+            pin.common.xml_insert_element(xml_doc, groups_xml, 'group', group)
+
+        return contact_xml
+
 class StoredContactList:
     def __init__(self, connection, channel_handle_obj):
         """
-- 
1.5.6.3




More information about the Telepathy-commits mailing list