[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