#!/usr/local/bin/python2.5

# pinocchio-ctl - manipulate an account on the telepathy-pinocchio connection
#                 manager
#
# Copyright (C) 2008 Nokia Corporation
# Copyright (C) 2008 Collabora Ltd.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# version 2.1 as published by the Free Software Foundation.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
# 02110-1301 USA

import getopt
import os.path
import sys

import dbus
from dbus.mainloop.glib import DBusGMainLoop
import gobject
from time import sleep

import telepathy as tp

import pinocchio as pin

contacts = {}

class Contact(tp.server.Handle):
    """
    Simple Contact wrapper of Handle to associate metadata with a Contact
    handle.
    """

    def __init__(self, id):
        tp.server.Handle.__init__(self, id, tp.constants.HANDLE_TYPE_CONTACT,
                                  'undefined name')
        self._avatar_bin = None
        self._avatar_mime = ''
        self._avatar_token = ''

def on_avatar_updated(handle_id, avatar_token):
    """Handle the Connection.Interface.Avatars::AvatarUpdated signal."""

def on_avatar_retrieved(handle_id, avatar_token, avatar_bin, avatar_mime):
    """Handle the Connection.Interface.Avatars::AvatarRetrieved signal."""

    # Track the avatar info for each contact
    contact = None
    if handle_id in contacts:
        contact = contacts[handle_id]
    else:
        contact = Contact(handle_id)
        contacts[handle_id] = contact

    if contact:
        contact._avatar_bin = avatar_bin
        contact._avatar_mime = avatar_mime
        contact._avatar_token = avatar_token

def avatars_request_all(connection, channels, avatars):
    """Request avatars for all contacts on all given contact list channels.
    
    Arguments:
    connection -- opened connection
    channels -- contact list channels for the given connection
    avatars -- Avatars interface on the given channel
    """
    for handle in channels:
        channel = channels[handle]

        proxy_obj = bus.get_object(connection.service_name, channel.object_path)
        channel_base_obj = dbus.Interface(proxy_obj, tp.interfaces.CHANNEL)

        handle_type, ignore = channel_base_obj.GetHandle()
        channel_name = connection.InspectHandles(handle_type, (handle,))[0]

        current, local_pending, remote_pending = channel.GetAllMembers()
        member_lists = {'current': current, 'local_pending': local_pending,
                        'remote_pending': remote_pending}
        
        for member_list_name in member_lists:
            #print '  {%s}' % member_list_name
            member_handles = [m for m in member_lists[member_list_name]]

            avatar_tokens = avatars.GetKnownAvatarTokens(member_handles)

            members_to_request = []
            for member_handle in avatar_tokens:
                # we already have an avatar for this contact
                if member_handle in contacts:
                    contact = contacts[member_handle]

                    if avatar_tokens[member_handle] != contact._avatar_token:
                        members_to_request.append(member_handle)
                else:
                    members_to_request.append(member_handle)

            # this will be handled in the signal handlers
            avatars.RequestAvatars(members_to_request)

def connection_manager_get(bus):
    """Returns the Pinocchio connection manager as a DBus Interface.
    
    Arguments:
    bus -- DBus SessionBus to use
    """
    proxy_obj = bus.get_object(pin.common.CM_PINOCCHIO,
                               pin.common.CM_PINOCCHIO_OBJ)

    manager = dbus.Interface(proxy_obj, tp.interfaces.CONNECTION_MANAGER)

    return manager

def connection_get_opened(bus, manager, params, opts):
    """Returns a newly-opened connection to the manager as a DBus Interface.
    
    Arguments:
    bus -- DBus SessionBus to use
    manager -- DBus Interface of the connection manager
    params -- dict of connection parameters
    opts -- command-line options to pinocchio-ctl

    Returns:
    connection -- newly-opened connection as a DBus Interface
    service -- connection's account DBus interface string
    proxy_obj -- the DBus object for this connection
    """
    connection = None

    connections = tp.client.Connection.get_connections(bus)
    for conn in connections:
        account_id = tp.server.conn._escape_as_identifier(params['account'])
        object_path = '/org/freedesktop/Telepathy/Connection/%s/%s/%s' % \
                                    ('pinocchio', 'dummy', account_id)

        if conn.object_path == object_path:
            connection = conn
            # add a flag to know whether we should disconnect on our way out
            connection.self_opened = False
            break

    if not connection:
        service, obj_path = manager.RequestConnection('dummy', params)
        proxy_obj = bus.get_object(service, obj_path)

        connection = tp.client.Connection(service, obj_path)
        # add a flag to know whether we should disconnect on our way out
        connection.self_opened = True

    if connection.GetStatus() == tp.constants.CONNECTION_STATUS_DISCONNECTED:
        connection.Connect()

    return (connection, connection.service_name,
            bus.get_object(connection.service_name, connection.object_path))

def contact_lists_get_all(bus, connection, service):
    """Returns the existing channels for the connection.
    
    Arguments:
    bus -- DBus SessionBus to use
    connection -- opened connection as a DBus Interface
    service -- service interface name

    Returns:
    channels -- dict of {handle integer: channel as a DBus Interface}
    """
    channels = {} 

    channels_existing = connection.ListChannels()
    for channel in channels_existing:
        channel_path, channel_iface, handle_type, handle = channel

        channel_proxy = bus.get_object(service, channel_path)
        # FIXME: use this actual interface, or change this function's return
        # type to be broader so we can do it correctly farther up the call stack
        #channel_iface_obj = dbus.Interface(channel_proxy, channel_iface)
        channel_iface_obj = dbus.Interface(
                                        channel_proxy,
                                        tp.interfaces.CHANNEL_INTERFACE_GROUP)

        channels[handle] = channel_iface_obj

    return channels

def contact_lists_print(connection, presence, aliasing, avatars, contact_lists):
    """Prints the members of the given contact list channels.
    
    Arguments:
    connection -- opened connection as a DBus Interface
    presence -- presence interface for the connection
    aliasing -- aliasing interface for the connection
    avatars -- avatars interface for the connection
    contact_lists -- iterable of opened contact list channels (DBus Interfaces)
    """
    printed_first_list = False
    for handle in contact_lists:
        # ensure we only output "\n\n\n" between contact lists; this will
        # make it easier to parse the output for validation
        if printed_first_list:
            print '\n\n',

        channel = contact_lists[handle]

        proxy_obj = bus.get_object(connection.service_name, channel.object_path)
        channel_base_obj = dbus.Interface(proxy_obj, tp.interfaces.CHANNEL)

        handle_type, ignore = channel_base_obj.GetHandle()
        channel_name = connection.InspectHandles(handle_type, (handle,))[0]

        print '[%s]' % channel_name

        current, local_pending, remote_pending = channel.GetAllMembers()
        member_lists = {'current': current, 'local_pending': local_pending,
                        'remote_pending': remote_pending}
        
        for member_list_name in member_lists:
            #print '  {%s}' % member_list_name
            member_handles = [m for m in member_lists[member_list_name]]
            presences = presence.GetPresence(member_handles)
            aliases = aliasing.RequestAliases(member_handles)

            # toss the aliases in a dictionary to simplify the next loop
            alias_dict = {}
            for (member_handle, alias) in zip(member_handles, aliases):
                alias_dict[member_handle] = alias

            printed_first_member = False
            for member_handle in member_handles:
                handle_type = tp.constants.HANDLE_TYPE_CONTACT
                member_name = connection.InspectHandles(handle_type,
                                                        (member_handle,))[0]

                # ensure we only output "\n\n" between contact lists; this will
                # make it easier to parse the output for validation
                if printed_first_member:
                    print

                # encode the alias (otherwise, unicode strings can hang any
                # calling command)
                print 'handle:       %d' % member_handle
                print 'username:     "%s"' % member_name
                print 'alias:        "',
                sys.stdout.write(alias_dict[member_handle].encode('utf-8'))
                print '"'

                # use of more than one status here is deprecated
                activity_status = presences[member_handle]
                # get the status (ignore the timestamp, [0]; it's deprecated)
                status_dict = activity_status[1]
                statuses = status_dict.keys()
                status_messages = status_dict.values()
                status_message = status_messages[0]['message']
                status = statuses[0]
                #print '        %s: %s' % (status, status_message)
                print 'status:       %s' % status
                print 'message:      %s' % status_message

                avatar_mime = ''
                avatar_token = ''
                if member_handle in contacts:
                    member_contact = contacts[member_handle]
                    avatar_mime = member_contact._avatar_mime
                    avatar_token = member_contact._avatar_token

                print 'avatar_mime:  %s' % avatar_mime
                print 'avatar_token: %s' % avatar_token

                printed_first_member = True

        printed_first_list = True

class UserCommand:
    """
    Utility class for representing commands issued by the user.
    """

    def __init__(self, username, attributes):
        self.attributes = {}

        self.username = username
        self.attributes = attributes

def print_help_exit(exit_status, pre_message=None):

    if pre_message:
        print "%s\n" % pre_message

    print """usage:
    pinocchio-ctl [options] COMMAND [args]

    options:
      -u USERNAME / --username=USERNAME
          connect via a non-default username

      -p PASSWORD / --password=PASSWORD
          specify a non-default password

    commands:
       list (print the unmodified contact list)
       reset (revert the contact list to its defaults)
       add-group GROUP_1 [GROUP_2] [...]
       remove-group GROUP_1 [GROUP_2] [...]
       add-account ACCOUNT_NAME
       remove-account ACCOUNT_NAME
       
       add-contact USERNAME:[lists=LIST_1][+LIST_2][+...] [...]

       supported keys:
          lists       -- contact lists to add contact to (eg, 'subscribe')

       remove-contact USERNAME:[lists=LIST_1][+LIST_2][+...] [...]

       supported keys:
          lists       -- contact lists to remove contact from (eg, 'subscribe')

       set USERNAME:[KEY_1=VALUE_1][,KEY_2=VALUE_2][,...] [...]

       supported keys:
          alias       -- server-side contact alias (eg, 'Jane Doe')
          avatar-file -- path to avatar image file (eg, '/tmp/face.png')
          message     -- presence status message (eg, 'I am out to lunch')
          status      -- presence status (eg, 'available', 'dnd', 'offline')

    eg:
       pinocchio-ctl set foo@barbaz.com:alias='Foo Bar Baz'

       pinocchio-ctl set foo@barbaz.com:status='busy',message='at lunch'

    """

    sys.exit(exit_status)

def command_list(opts, args, mainloop):
    """Print out the full contact lists.

    Arguments:
    opts -- command-line options
    args -- command arguments in the form:
            USERNAME:[(status=STATUS|message=MESSAGE|alias=ALIAS)][...] [...]
            (ignored)
    mainloop -- the gobject mainloop to use for signals
    """
    bus = dbus.SessionBus()
    manager = connection_manager_get(bus)

    conn_params = {'account': opts['username'], 'password': opts['password']}
    connection, service, proxy_obj = connection_get_opened(bus, manager,
                                                           conn_params, opts)

    channels = contact_lists_get_all(bus, connection, service)

    presence = dbus.Interface(proxy_obj,
                              tp.interfaces.CONNECTION_INTERFACE_PRESENCE)
    aliasing = dbus.Interface(proxy_obj,
                              tp.interfaces.CONNECTION_INTERFACE_ALIASING)
    avatars = dbus.Interface(proxy_obj,
                             tp.interfaces.CONNECTION_INTERFACE_AVATARS)

    avatars_request_all(connection, channels, avatars)

    # allow signals to propagate and be handled, then quit
    gobject.timeout_add(1000, command_print_lists_finalize, mainloop,
                        connection, '', presence, aliasing, avatars, channels)

def command_print_lists_finalize(mainloop, connection, pre_message,
                                 presence, aliasing, avatars, channels):
    """Print out final contact lists, close connections, and quit.

    Arguments:
    mainloop -- GObject mainloop
    connection -- Telepathy Connection to the connection manager
    pre_message -- an optional message to print before the lists
    presence -- connection's Presence interface
    aliasing -- connection's Aliasing interface
    avatars -- connection's Avatars interface
    channels -- connection's ContactList channels
    """
    if pre_message:
        print pre_message

    # update our known contact list and group channels, in case they changed
    bus = dbus.SessionBus()
    channels = contact_lists_get_all(bus, connection, connection.service_name)

    contact_lists_print(connection, presence, aliasing, avatars, channels)

    if connection.self_opened:
        connection.Disconnect()
    mainloop.quit()

def command_add_contact(opts, args, mainloop):
    """Add any number of contacts to given contact lists."""
    command_add_remove_contact('add-contact', opts, args, mainloop)

def command_remove_contact(opts, args, mainloop):
    """Remove any number of contacts to given contact lists."""
    command_add_remove_contact('remove-contact', opts, args, mainloop)

# This chain of functions handles both add-contact and remove-contact, since
# they require nearly-identically routines
def command_add_remove_contact(command, opts, args, mainloop):
    """Add or remove any number of contacts to given contact lists.

    Arguments:
    opts -- command-line options
    args -- command arguments in the form:
            USERNAME:[lists=LIST_1][+LIST_2][+...] [...]
    mainloop -- the gobject mainloop to use for signals
    """
    pre_message_mangled = "invalid arguments for command '%s'" % command

    if command not in ('add-contact', 'remove-contact'):
        print_help_exit(6, 'invalid command for command_add_remove_contact()')

    subcommands = []
    for arg in args:
        arg = arg.strip()

        try:
            username, user_commands = arg.split(':')
            user_commands = user_commands.split(',')

            attributes = {}
            for user_command in user_commands:
                key, value = user_command.split('=')
                attributes[key] = value

            user = UserCommand(username, attributes)

            subcommands.append(user)
        except ValueError:
            print_help_exit(3, pre_message_mangled)


    bus = dbus.SessionBus()
    manager = connection_manager_get(bus)

    conn_params = {'account': opts['username'], 'password': opts['password']}
    connection, service, proxy_obj = connection_get_opened(bus, manager,
                                                           conn_params, opts)

    channels = contact_lists_get_all(bus, connection, service)

    presence = dbus.Interface(proxy_obj,
                              tp.interfaces.CONNECTION_INTERFACE_PRESENCE)
    aliasing = dbus.Interface(proxy_obj,
                              tp.interfaces.CONNECTION_INTERFACE_ALIASING)
    avatars = dbus.Interface(proxy_obj,
                             tp.interfaces.CONNECTION_INTERFACE_AVATARS)

    avatars_request_all(connection, channels, avatars)

    # allow signals to propagate and be handled
    gobject.timeout_add(2000, command_add_remove_contact_continue, mainloop,
                        command, subcommands, connection, presence, aliasing,
                        avatars, channels)

def command_add_remove_contact_continue(mainloop, command, subcommands,
                                        connection, presence, aliasing, avatars,
                                        channels):
    """Continue the 'add-contact' or 'remove-contact' command after initial
    signals have been handled.

    Arguments:
    mainloop -- GObject mainloop
    commands -- 'add-contact' or 'remove-contact' to determine which command
                we're performing
    subcommands -- list of UserCommand objects parsed from command arguments
    connection -- Telepathy Connection to the connection manager
    presence -- connection's Presence interface
    aliasing -- connection's Aliasing interface
    avatars -- connection's Avatars interface
    channels -- connection's ContactList channels
    """
    print
    print "original contact list:"
    print "------------------------------------"
    contact_lists_print(connection, presence, aliasing, avatars, channels)

    print "<updating contact lists>\n"

    channel_map = {}
    for handle in channels:
        channel = channels[handle]

        proxy_obj = bus.get_object(connection.service_name, channel.object_path)
        channel_base_obj = dbus.Interface(proxy_obj, tp.interfaces.CHANNEL)

        handle_type, ignore = channel_base_obj.GetHandle()
        channel_name = connection.InspectHandles(handle_type, (handle,))[0]

        channel_map[channel_name] = channels[handle]

    # update contact lists
    for subcommand in subcommands:
        contact_handles = \
                    connection.RequestHandles(tp.constants.HANDLE_TYPE_CONTACT,
                                              (subcommand.username,))
        if len(contact_handles) < 1:
            print 'warning: failed to get a handle for contact "%s"' % \
                                                            subcommand.username
            continue

        contact_handle = contact_handles[0]

        if 'lists' not in subcommand.attributes:
            print_help_exit(7, pre_message_mangled)

        lists = subcommand.attributes['lists'].split('+')
        for list in lists:
            if list in channel_map:
                if   command == 'add-contact':
                    channel_map[list].AddMembers((contact_handle,), '')
                elif command == 'remove-contact':
                    channel_map[list].RemoveMembers((contact_handle,), '')
            else:
                print 'warning: skipping username "%s" and channel ' \
                      '"%s": channel does not exist' % (subcommand.username,
                                                        list)

    pre_message  = "updated contact list:\n------------------------------------"
    # allow signals to propagate and be handled, then quit
    gobject.timeout_add(3000, command_print_lists_finalize, mainloop,
                        connection, pre_message, presence, aliasing, avatars,
                        channels)

def command_add_group(opts, args, mainloop):
    """Add any number of user-defined, server-side groups."""
    command_add_remove_group('add-group', opts, args, mainloop)

def command_remove_group(opts, args, mainloop):
    """Remove any number of user-defined, server-side groups."""
    command_add_remove_group('remove-group', opts, args, mainloop)

# This chain of functions handles both add-group and remove-group, since they
# require nearly-identical routines
def command_add_remove_group(command, opts, args, mainloop):
    """Add or remove any number of user-defined, server-side groups.

    Arguments:
    opts -- command-line options
    args -- command arguments in the form:
            GROUP_1 [GROUP_2] [...]
    mainloop -- the gobject mainloop to use for signals
    """
    pre_message_mangled = "invalid arguments for command '%s'" % command

    if command not in ('add-group', 'remove-group'):
        print_help_exit(6, 'invalid command for command_add_remove_group()')

    subcommands = []
    for arg in args:
        arg = arg.strip()

        # FIXME: it's not quite appropriate to use a "UserCommand" to handle
        # group work; change names as necessary to make this less awkward. Maybe
        # just make a GroupCommand that only takes a single arg for now
        group = UserCommand(arg, {})
        subcommands.append(group)

    bus = dbus.SessionBus()
    manager = connection_manager_get(bus)

    conn_params = {'account': opts['username'], 'password': opts['password']}
    connection, service, proxy_obj = connection_get_opened(bus, manager,
                                                           conn_params, opts)

    channels = contact_lists_get_all(bus, connection, service)

    presence = dbus.Interface(proxy_obj,
                              tp.interfaces.CONNECTION_INTERFACE_PRESENCE)
    aliasing = dbus.Interface(proxy_obj,
                              tp.interfaces.CONNECTION_INTERFACE_ALIASING)
    avatars = dbus.Interface(proxy_obj,
                             tp.interfaces.CONNECTION_INTERFACE_AVATARS)

    avatars_request_all(connection, channels, avatars)

    # allow signals to propagate and be handled
    gobject.timeout_add(2000, command_add_remove_group_continue, mainloop,
                        command, subcommands, connection, presence, aliasing,
                        avatars, channels)

def command_add_remove_group_continue(mainloop, command, subcommands,
                                      connection, presence, aliasing, avatars,
                                      channels):
    """Continue the 'add-group' or 'remove-group' command after initial
    signals have been handled.

    Arguments:
    mainloop -- GObject mainloop
    commands -- 'add-group' or 'remove-group' to determine which command we're
                performing
    subcommands -- list of UserCommand objects parsed from command arguments
    connection -- Telepathy Connection to the connection manager
    presence -- connection's Presence interface
    aliasing -- connection's Aliasing interface
    avatars -- connection's Avatars interface
    channels -- connection's ContactList channels
    """
    print
    print "original contact list:"
    print "------------------------------------"
    contact_lists_print(connection, presence, aliasing, avatars, channels)

    print "<updating contact lists>\n"

    # FIXME: change the variable name so it makes sense here too
    # XXX: here, subcommand.username actually refers to the group name
    
    # add or remove groups
    for subcommand in subcommands:
        group_handles = \
                    connection.RequestHandles(tp.constants.HANDLE_TYPE_GROUP,
                                              (subcommand.username,))
        if len(group_handles) < 1:
            print 'warning: failed to get a handle for group "%s"' % \
                                                            subcommand.username
            continue

        group_handle = group_handles[0]

        # hint that other clients may handle this channel
        suppress_handler = False
        if   command == 'add-group':
            connection.RequestChannel(tp.interfaces.CHANNEL_TYPE_CONTACT_LIST,
                                      tp.constants.HANDLE_TYPE_GROUP,
                                      group_handle, suppress_handler)
        elif command == 'remove-group':
            # get the existing channel
            group_path = connection.RequestChannel(
                                        tp.interfaces.CHANNEL_TYPE_CONTACT_LIST,
                                        tp.constants.HANDLE_TYPE_GROUP,
                                        group_handle, suppress_handler)

            proxy_obj = bus.get_object(connection.service_name,
                                       group_path)
            group_obj = dbus.Interface(proxy_obj,
                                       tp.interfaces.CHANNEL_INTERFACE_GROUP)
            channel_obj = dbus.Interface(proxy_obj, tp.interfaces.CHANNEL)

            # all existing members need to be removed before removing the group
            current, local_pending, remote_pending = group_obj.GetAllMembers()

            group_obj.RemoveMembers(current + local_pending + remote_pending,
                                    '')

            # effectively remove the group
            channel_obj.Close()

    pre_message  = "updated contact list:\n------------------------------------"
    # allow signals to propagate and be handled, then quit
    gobject.timeout_add(3000, command_print_lists_finalize, mainloop,
                        connection, pre_message, presence, aliasing, avatars,
                        channels)

def command_set_unified(opts, args, mainloop):
    """Set live attributes for any number of contacts.

    Arguments:
    opts -- command-line options
    args -- command arguments in the form:
            USERNAME:[(status=STATUS|message=MESSAGE|alias=ALIAS)][...] [...]
    mainloop -- the gobject mainloop to use for signals
    """
    pre_message_mangled = "invalid arguments for command 'set'"


    subcommands = []
    for arg in args:
        arg = arg.strip()

        try:
            username, user_commands = arg.split(':')
            user_commands = user_commands.split(',')

            attributes = {}
            for user_command in user_commands:
                key, value = user_command.split('=')
                attributes[key] = value

            user = UserCommand(username, attributes)

            subcommands.append(user)
        except ValueError:
            print_help_exit(3, pre_message_mangled)


    bus = dbus.SessionBus()
    manager = connection_manager_get(bus)

    conn_params = {'account': opts['username'], 'password': opts['password']}
    connection, service, proxy_obj = connection_get_opened(bus, manager,
                                                           conn_params, opts)

    channels = contact_lists_get_all(bus, connection, service)

    presence = dbus.Interface(proxy_obj,
                              tp.interfaces.CONNECTION_INTERFACE_PRESENCE)
    aliasing = dbus.Interface(proxy_obj,
                              tp.interfaces.CONNECTION_INTERFACE_ALIASING)
    avatars = dbus.Interface(proxy_obj,
                             tp.interfaces.CONNECTION_INTERFACE_AVATARS)

    avatars_request_all(connection, channels, avatars)

    # allow signals to propagate and be handled
    gobject.timeout_add(2000, command_set_unified_continue, mainloop,
                        subcommands, connection, presence, aliasing, avatars,
                        channels)

def command_set_unified_continue(mainloop, subcommands, connection, presence,
                                 aliasing, avatars, channels):
    """Continue the 'set' command after initial signals have been handled.

    Arguments:
    mainloop -- GObject mainloop
    subcommands -- list of UserCommand objects parsed from command arguments
    connection -- Telepathy Connection to the connection manager
    presence -- connection's Presence interface
    aliasing -- connection's Aliasing interface
    avatars -- connection's Avatars interface
    channels -- connection's ContactList channels
    """
    print
    print "original contact list:"
    print "------------------------------------"
    contact_lists_print(connection, presence, aliasing, avatars, channels)

    #print "<delay for demo purposes>\n"
    #sleep(8)

    print "<updating attributes>\n"

    # update presences
    new_presences = {}
    for subcommand in subcommands:
        # If both status and message were omitted, don't change either for this
        # person
        if 'status' not in subcommand.attributes \
            and 'message' not in subcommand.attributes:
            continue

        if 'status' not in subcommand.attributes:
            subcommand.attributes['status'] = 'offline'
        if 'message' not in subcommand.attributes:
            subcommand.attributes['message'] = ''

        new_presences[subcommand.username] = (subcommand.attributes['status'],
                                              subcommand.attributes['message'])
    presence.set_presences(new_presences)

    #FIXME: request the handles all at once
    # update aliases
    new_aliases = {}
    for subcommand in subcommands:
        # if unspecified, don't change the alias for this person
        if 'alias' not in subcommand.attributes:
            continue

        handle_ids = connection.RequestHandles(tp.constants.HANDLE_TYPE_CONTACT,
                                               (subcommand.username,))
        if len(handle_ids) > 0:
            handle_id = handle_ids[0]

        if handle_id > 0:
            new_aliases[handle_id] = subcommand.attributes['alias']
        else:
            print "warning: got invalid handle ID 0 for username '%s'" % \
                                                            subcommand.username
    aliasing.SetAliases(new_aliases)

    # FIXME: combine this RequestHandles with the one above; just cache the
    # results
    # set avatars
    for subcommand in subcommands:
        # if unspecified, don't change the avatar for this person
        if 'avatar-file' not in subcommand.attributes:
            continue

        handle_ids = connection.RequestHandles(tp.constants.HANDLE_TYPE_CONTACT,
                                               (subcommand.username,))
        if len(handle_ids) > 0:
            handle_id = handle_ids[0]

        if handle_id > 0:
            avatars.set_avatar(handle_id, subcommand.attributes['avatar-file'])
        else:
            print "warning: got invalid handle ID 0 for username '%s'" % \
                                                            subcommand.username

    avatars_request_all(connection, channels, avatars)

    #print "<delay for demo purposes>\n"
    #sleep(4)

    pre_message  = "updated contact list:\n------------------------------------"
    # allow signals to propagate and be handled, then quit
    gobject.timeout_add(3000, command_print_lists_finalize, mainloop,
                        connection, pre_message, presence, aliasing, avatars,
                        channels)

def command_reset(opts, args, mainloop):
    """Reset the contact list to its default state.

    Arguments:
    opts -- command-line options
    args -- currently ignored
    mainloop -- the gobject mainloop to use for signals
    """
    pre_message_mangled = "'reset' command does not take any arguments"

    if len(args) >= 1:
        print_help_exit(5, pre_message_mangled)

    bus = dbus.SessionBus()
    manager = connection_manager_get(bus)

    conn_params = {'account': opts['username'], 'password': opts['password']}
    connection, service, proxy_obj = connection_get_opened(bus, manager,
                                                           conn_params, opts)

    connection.reset_to_default_contacts_file()

    print "contact list reverted to defaults"

    if connection.self_opened:
        connection.Disconnect()
    mainloop.quit()

def command_add_account(opts, args, mainloop):
    """Create a new pinocchio/dummy account.

    Arguments:
    opts -- command-line options
    args -- currently ignored
    mainloop -- the gobject mainloop to use for signals
    """
    pre_message_mangled = "'reset' command does not take any arguments"

    if len(args) != 1:
        print_help_exit(10, pre_message_mangled)

    account_name = args[0]
    bus = dbus.SessionBus()
    manager = connection_manager_get(bus)

    manager.create_account(account_name)

    print "new account '%s' created" % account_name

    mainloop.quit()

def command_remove_account(opts, args, mainloop):
    """Delete a local pinocchio/dummy account. Won't work for globally-installed
    accounts.

    Arguments:
    opts -- command-line options
    args -- currently ignored
    mainloop -- the gobject mainloop to use for signals
    """
    pre_message_mangled = "'reset' command does not take any arguments"

    if len(args) != 1:
        print_help_exit(10, pre_message_mangled)

    account_name = args[0]
    bus = dbus.SessionBus()
    manager = connection_manager_get(bus)

    try:
        manager.remove_account(account_name)
        print "account '%s' removed" % account_name
    except:
        print "failed to remove account '%s'" % account_name

    mainloop.quit()

def signal_handlers_init(bus):
    """Set up handlers for the signals we're interested in.
    
    Arguments:
    bus -- DBus Session Bus to use
    """
    bus.add_signal_receiver(
                    on_avatar_updated,
                    dbus_interface = tp.interfaces.CONNECTION_INTERFACE_AVATARS,
                    signal_name = "AvatarUpdated")

    bus.add_signal_receiver(
                    on_avatar_retrieved,
                    dbus_interface = tp.interfaces.CONNECTION_INTERFACE_AVATARS,
                    signal_name = "AvatarRetrieved")

    
COMMANDS = {'list': command_list,
            'add-contact': command_add_contact,
            'remove-contact': command_remove_contact,
            'add-group': command_add_group,
            'remove-group': command_remove_group,
            'set': command_set_unified,
            'reset': command_reset,
            'add-account': command_add_account,
            'remove-account': command_remove_account}

def validate_args(argv):
    """Ensure that the given command-line arguments are valid and return their
    components. Prints the help text and exits in case of failure.

    Arguments:
    argv -- the standard argument vector (ie, sys.argv)

    Returns:
    command, command_opts, command_args
    """
    if len(sys.argv) <= 1:
        print_help_exit(1)

    command_opts = {'username': pin.common.ACCOUNT_DEFAULT,
                    'password': ''}
    try:
        opts, args = getopt.getopt(sys.argv[1:], "p:u:",
                                   ["password=", "username="])
    except getopt.GetoptError, err:
        print_help_exit(2, pre_message=str(err))
    
    for option, arg in opts:
        if   option in ("-p", "--password"):
            command_opts['password'] = arg
        elif option in ("-u", "--username"):
            command_opts['username'] = arg
        else:
            print_help_exit(3, pre_message=str('unsupported option'))

    ### validate argument values ###
    account_id = tp.server.conn._escape_as_identifier(command_opts['username'])

    command = args[0]
    command_args = args[1:]

    if command not in (COMMANDS.keys() + ['help']):
        print_help_exit (99, "command '%s' unknown" % command)

    return command, command_opts, command_args

if __name__ == '__main__':
    command, command_opts, command_args = validate_args(sys.argv)

    if command == 'help':
        print_help_exit(0)

    #else
    DBusGMainLoop(set_as_default=True)
    bus = dbus.SessionBus()

    signal_handlers_init(bus)

    mainloop = gobject.MainLoop()

    # prep our core command; kind of a hack to cope with the fact that we're
    # forced to use a mainloop for signal handling
    gobject.timeout_add(500, COMMANDS[command], command_opts, command_args,
                        mainloop)

    mainloop.run()
