#!/usr/bin/python
# vi:si:et:sw=4:sts=4:ts=4
# Copyright 2009 Canonical Ltd.
#
# This file is part of desktopcouch.
#
#  desktopcouch is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation.
#
# desktopcouch 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 desktopcouch.  If not, see <http://www.gnu.org/licenses/>.
# vim: filetype=python expandtab smarttab

__doc__ = """\
Manage paired machines.

A tool to set two local machines to replicate their couchdb instances to each
other, or to set this machine to replicate to-and-from Ubuntu One (and perhaps
other cloud services).  

Local-Pairing Authentication
----------------------------

One machine, Alice, sets herself to listen for invitations to pair with another
machine.  In doing so, Alice is discoverable via Zeroconf on local network.

Another machine, Bob, sees the advertisement and generates a secret message and
a public seed.  Bob then computes the SHA512 digest of the secret, and sends an
invitation to Alice, in the form of the hex digest concatenated with the public
seed.  Bob displays the secret to the user, who walks the secret over to Alice.

Alice then computes the SHA512 digest of Bob's secret and compares it to be
sure that the other machine is indeed the user's.  Alice then concatenates
Bob's secret and the public seed, and sends the resulting hex digest back to 
Bob to prove that she received the secret from the user.  Alice sets herself to
replicate to Bob.

Bob computes the secret+publicseed digest and compares it with the received hex
digest from Alice.  When it matches, he sets himself to replicate to Alice.
"""



import logging
import getpass
import gettext
# gettext implements "_" function.  pylint: disable-msg=E0602
import random
import cgi

from twisted.internet import gtk2reactor
gtk2reactor.install()
from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)
from twisted.internet.reactor import run as run_program
from twisted.internet.reactor import stop as stop_program

import pygtk
pygtk.require('2.0')
import gtk
import gobject
import pango

from desktopcouch.pair.couchdb_pairing import couchdb_io
from desktopcouch.pair.couchdb_pairing import network_io
from desktopcouch.pair.couchdb_pairing import dbus_io
from desktopcouch.pair.couchdb_pairing import couchdb_io
from desktopcouch.pair import pairing_record_type
from desktopcouch import local_files

from desktopcouch.records.record import Record

DISCOVERY_TOOL_VERSION = "1"

def generate_secret(length=7):
    """Create a secret that is easy to write and read.  We hate ambiguity and
    errors."""

    pool = "abcdefghijklmnopqrstuvwxyz23456789#$%&*+=-"
    return unmap_easily_mistaken(
            "".join(random.choice(pool) for n in range(length)))

def unmap_easily_mistaken(user_input):
    """Returns ASCII-encoded text with visually-ambiguous characters crushed
    down to some common atom."""

    import string
    easily_mistaken = string.maketrans("!10@", "lloo")
    return string.translate(user_input.lower().encode("ascii"), easily_mistaken)

def get_host_info():
    """Create some text that hopefully identifies this host out of many."""

    import platform

    try:
        uptime_seconds = int(float(file("/proc/uptime").read().split()[0]))
        days, uptime_seconds = divmod(uptime_seconds, 60*60*24)
        hours, uptime_seconds = divmod(uptime_seconds, 60*60)
        minutes, seconds = divmod(uptime_seconds, 60)

        # Is ISO8601 too nerdy? ...
        #uptime_descr = ", up PT%(days)dD%(hours)dH%(minutes)dM" % locals()
        uptime_descr = ", up %(days)dd %(hours)dh%(minutes)dm" % locals()
    except OSError:
        uptime_descr = ""

    try:
        return " ".join(platform.dist()) + uptime_descr
    except AttributeError:
        return platform.platform() + uptime_descr


class Inviting:
    """We're part of "Bob" in the module's story.

    We see listeners on the network and send invitations to pair with us.
    We generate a secret message and a public seed.  We get the SHA512 hex
    digest of the secret message, append the public seed and send it to 
    Alice.  We also display the cleartext of the secret message to the 
    screen, so that the user can take it to the machine he thinks is Alice.

    Eventually, we receive a message back from Alice.  We compute the 
    secret message we started with concatenated with the public seed, and 
    if that matches Alice's message, then Alice must know the secret we 
    displayed to the user.  We then set outselves to replicate to Alice."""

    def delete_event(self, widget, event, data=None):
        """User requested window be closed.  False to propogate event."""
        return False

    def destroy(self, widget, data=None):
        """The window is destroyed."""
        self.inviter.close()

    def auth_completed(self, remote_host, remote_id, remote_oauth):
        """The auth stage is finished.  Now pair with the remote host."""
        pair_with_host(remote_host, remote_id, remote_oauth, self.parent)
        self.window.destroy()

    def on_close(self):
        """When a socket is closed, we should stop inviting.  (?)"""
        self.window.destroy()

    def __init__(self, service, hostname, port, parent):
        self.logging = logging.getLogger(self.__class__.__name__)

        self.hostname = hostname
        self.port = port
        self.parent = parent

        self.window = gtk.Window()
        self.window.set_border_width(6)
        self.window.connect("delete_event", self.delete_event)
        self.window.connect("destroy", self.destroy)
        self.window.set_destroy_with_parent(True)
        self.window.set_title(
                _("Inviting %s to pair for CouchDB Pairing") % hostname)

        secret_message = generate_secret()
        self.secret_message = secret_message
        self.public_seed = generate_secret()

        self.inviter = network_io.start_send_invitation(hostname, port, 
                self.auth_completed, self.secret_message, self.public_seed,
                self.on_close, couchdb_io.get_my_host_unique_id(create=True)[0],
                local_files.get_oauth_tokens())

        top_vbox = gtk.VBox()
        self.window.add(top_vbox)

        text = gtk.Label()
        text.set_markup(_("""We're inviting %s to pair with\n""" +
                """us, and to prove veracity of the invitation we\n""" +
                """sent, it is waiting for you to tell it this secret:\n""" +
                """<span font-size="xx-large" color="blue" weight="bold">""" +
                """<tt>%s</tt></span> .""") % 
                        (cgi.escape(service), cgi.escape(self.secret_message)))
        text.set_justify(gtk.JUSTIFY_CENTER)
        top_vbox.pack_start(text, False, False, 10)
        text.show()

        cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL)
        cancel_button.set_border_width(3)
        cancel_button.connect("clicked", lambda widget: self.window.destroy())
        top_vbox.pack_end(cancel_button, False, False, 10)
        cancel_button.show()

        self.window.show_all()


class AcceptInvitation:
    """We're part of 'Alice' in this module's story.
    
    We've received an invitation.  We now send the other end a secret key.  The
    secret should make its way back to us via meatspace.  We open a dialog
    asking for that secret here, which we validate.

    When we validate it, we add this end as replicating to the other end.  The
    other end should also set itself to replicate to us."""

    def delete_event(self, widget, event, data=None):
        """User requested window be closed.  False to propogate event."""
        return False

    def destroy(self, widget, data=None):
        """Window is destroyed."""
        pass

    def on_close(self):
        """Handle communication channel being closed."""
        # FIXME   this is unimplemented and unused.
        self.destroy()

    def __init__(self, remote_host, is_secret_valid, send_valid_key,
            remote_hostid, remote_oauth):
        self.logging = logging.getLogger(self.__class__.__name__)

        self.is_secret_valid = is_secret_valid
        self.send_valid_key = send_valid_key

        self.window = gtk.Window()
        self.window.set_border_width(6)

        self.logging.info("want to listen for invitations.")

        self.window.connect("delete_event", self.delete_event)
        self.window.connect("destroy", self.destroy)
        self.window.set_destroy_with_parent(True)
        self.window.set_title(_("Accepting Invitation"))

        top_vbox = gtk.VBox()
        top_vbox.show()
        self.window.add(top_vbox)

        self.remote_hostname = dbus_io.get_remote_hostname(remote_host)
        self.remote_hostid = remote_hostid
        self.remote_oauth = remote_oauth

        description = gtk.Label(
                _("To verify your pairing with %s, enter its secret.") %
                        self.remote_hostname)
        description.show()
        top_vbox.pack_start(description, False, False, 0)

        self.entry_box = gtk.Entry(18)
        self.entry_box.connect("activate", lambda widget: self.verify_secret())
        #self.window.connect("activate", lambda widget: self.verify_secret())
        self.entry_box.set_activates_default(True)
        self.entry_box.show()
        top_vbox.pack_start(self.entry_box, False, False, 0)

        self.result = gtk.Label("")
        self.result.show()
        self.entry_box.connect("changed", 
                lambda widget: self.result.set_text(""))
        top_vbox.pack_start(self.result, False, False, 0)

        button_bar = gtk.HBox(homogeneous=True)
        button_bar.show()
        top_vbox.pack_end(button_bar, False, False, 10)

        cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL)
        cancel_button.set_border_width(3)
        cancel_button.connect("clicked", lambda widget: self.window.destroy())
        button_bar.pack_end(cancel_button, False, False, 10)
        cancel_button.show()

        connect_button = gtk.Button(_("Verify and connect"))
        add_image = gtk.Image()
        add_image.set_from_stock(gtk.STOCK_CONNECT, gtk.ICON_SIZE_BUTTON)
        connect_button.set_image(add_image)
        connect_button.set_border_width(3)
        connect_button.connect("clicked", lambda widget: self.verify_secret())
        button_bar.pack_end(connect_button, False, False, 10)
        connect_button.show()

        self.window.show_all()

    def verify_secret(self):
        """We got a sumbission from the user's fingers as to what he thinks
        the secret is.  We verify it, and if it's what the other end started
        with, then it's okay to let us pair with it."""

        proposed_secret = unmap_easily_mistaken(self.entry_box.get_text())
        if self.is_secret_valid(proposed_secret):
            pair_with_host(self.remote_hostname, self.remote_hostid,
                    self.remote_oauth)
            self.send_valid_key(proposed_secret)
            self.window.destroy()
        else:
            self.result.set_text("sorry, that is wrong")


class Listening:
    """We're part of 'Alice' in this module's story.
    
    Window that starts listening for other machines to pick *us* to pair
    with.  There must be at least one of these on the network for pairing to
    happen.  We listen for a finite amount of time, and then stop."""

    def delete_event(self, widget, event, data=None):
        """User requested window be closed.  False to propogate event."""
        return False

    def destroy(self, widget, data=None):
        """Window is destroyed."""
        self.timeout_counter = None

        if self.advertisement is not None:
            self.advertisement.die()
        self.listener.close()

    def receive_invitation_challenge(self, remote_address, is_secret_valid,
            send_secret, remote_hostid, remote_oauth):
        """When we receive an invitation, check its validity and if
        it's what we expected, then continue and accept it."""

        self.logging.warn("received invitation from %s", remote_address)
        self.acceptor = AcceptInvitation(remote_address, is_secret_valid,
                send_secret, remote_hostid, remote_oauth)
        self.acceptor.window.connect("destroy",
                lambda *args: setattr(self, "acceptor", None) and False)

    def cancel_acceptor(self):
        """Destroy window when connection canceled."""
        self.acceptor.window.destroy()

    def make_listener(self, couchdb_instance):
        """Start listening for connections, and start advertising that
        we're listening."""

        self.listener = network_io.ListenForInvitations(
                self.receive_invitation_challenge, 
                lambda: self.window.destroy(),
                couchdb_io.get_my_host_unique_id(create=True)[0],
                local_files.get_oauth_tokens())

        listen_port = self.listener.get_local_port()

        hostname, domainname = dbus_io.get_local_hostname()
        username = getpass.getuser()
        self.advertisement = dbus_io.PairAdvertisement(port=listen_port,
                name="%s-%s-%d" % (hostname, username, listen_port),
                text=dict(version=str(DISCOVERY_TOOL_VERSION),
                        description=get_host_info()))
        self.advertisement.publish()
        return hostname, username, listen_port

    def __init__(self, couchdb_instance):
        self.logging = logging.getLogger(self.__class__.__name__)

        self.listener = None
        self.listener_loop = None
        self.advertisement = None
        self.acceptor = None

        self.timeout_counter = 180

        self.window = gtk.Window()
        self.window.set_border_width(6)

        self.logging.info("want to listen for invitations.")

        self.window.connect("delete_event", self.delete_event)
        self.window.connect("destroy", self.destroy)
        self.window.set_destroy_with_parent(True)
        self.window.set_title(_("Waiting for CouchDB Pairing Invitations"))

        top_vbox = gtk.VBox()
        top_vbox.show()
        self.window.add(top_vbox)

        self.counter_text = gtk.Label("#")
        self.counter_text.show()

        top_vbox.pack_end(self.counter_text, False, False, 5)

        button_bar = gtk.HBox(homogeneous=True)
        button_bar.show()
        top_vbox.pack_end(button_bar, False, False, 10)

        cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL)
        cancel_button.set_border_width(3)
        cancel_button.connect("clicked", lambda *args: self.window.destroy())
        button_bar.pack_end(cancel_button, False, False, 10)
        cancel_button.show()

        add_minute_button = gtk.Button(_("Add 60 seconds"))
        add_image = gtk.Image()
        add_image.set_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)
        add_minute_button.set_image(add_image)
        add_minute_button.set_border_width(3)
        add_minute_button.connect("clicked",
                lambda widget: self.add_to_timeout_counter(60))
        button_bar.pack_end(add_minute_button, False, False, 10)
        add_minute_button.show()

        hostid, userid, listen_port = self.make_listener(couchdb_instance)

        text = gtk.Label()
        text.set_markup(
                _("""We're listening for invitations!  From another\n""" +
                    """machine on this local network, run this\n""" + 
                    """same tool and find the machine called\n""" + 
                    """<span font-size="xx-large" weight="bold"><tt>""" + 
                    """%s-%s-%d</tt></span> .""") % 
                            (cgi.escape(hostid), cgi.escape(userid), 
                            listen_port))
        text.set_justify(gtk.JUSTIFY_CENTER)
        top_vbox.pack_start(text, False, False, 10)
        text.show()
    
        self.update_counter_view()
        self.window.show_all()

        gobject.timeout_add(1000, self.decrement_counter)

    def add_to_timeout_counter(self, seconds):
        """The user wants more time.  Add seconds to the clock."""
        self.timeout_counter += seconds
        self.update_counter_view()

    def update_counter_view(self):
        """Update the counter widget with pretty text."""
        self.counter_text.set_text(
                _("%d seconds remaining") % self.timeout_counter)

    def decrement_counter(self):
        """Tick!  Decrement the counter and update the display."""
        if self.timeout_counter is None:
            return False

        self.timeout_counter -= 1
        if self.timeout_counter < 0:
            self.window.destroy()
            return False

        self.update_counter_view()
        return True
        

class PickOrListen:
    """Main top-level window that represents the life of the application."""

    def delete_event(self, widget, event, data=None):
        """User requested window be closed.  False to propogate event."""
        return False

    def destroy(self, widget, data=None):
        """The window was destroyed."""
        stop_program()

    def create_pick_pane(self, container):
        """Set up the pane that contains what's necessary to choose an
        already-listening tool instance.  This sets up a "Bob" in the 
        module's story."""

        # positions:                 host id, descr, host, port, cloud_name
        self.listening_hosts = gtk.TreeStore(str,   str,   str, int,   str)

        import desktopcouch.replication_services as services

        for srv_name in dir(services):
            if srv_name.startswith("__"):
                continue
            srv = getattr(services, srv_name)
            try:
                if srv.is_active():
                    all_paired_cloud_servers = [x.key for x in 
                        couchdb_io.get_pairings()]
                    if not srv_name in all_paired_cloud_servers:
                        self.listening_hosts.append(None, 
                                [srv.name, srv.description, "", 0, srv_name])
            except Exception, e:
                self.logging.exception("service %r has errors", srv_name)

        self.inviting = None  # pylint: disable-msg=W0201

        hostname_col = gtk.TreeViewColumn(_("hostname"))
        hostid_col = gtk.TreeViewColumn(_("service name"))
        description_col = gtk.TreeViewColumn(_("description"))

        pick_box = gtk.VBox()
        container.pack_start(pick_box, False, False, 10)

        l = gtk.Label(_("Pick a listening host to invite it to pair with us."))
        pick_box.pack_start(l, False, False, 0)
        l.show()

        tv = gtk.TreeView(self.listening_hosts)
        tv.set_headers_visible(False)
        tv.set_rules_hint(True)
        tv.show()

        def clicked(selection):
            """An item in the list of services was clicked, so now we go 
            about inviting it to pair with us."""

            model, iter = selection.get_selected()
            if not iter:
                return
            service = model.get_value(iter, 0)
            description = model.get_value(iter, 1)
            hostname = model.get_value(iter, 2)
            port = model.get_value(iter, 3)
            service_name = model.get_value(iter, 4)
            
            if service_name:
                # Pairing with a cloud service, which doesn't do key exchange
                pair_with_cloud_service(service_name, self.window)
                # remove from listening list
                self.listening_hosts.remove(iter)
                # add to already-paired list
                srv = getattr(services, service_name)
                self.already_paired_hosts.append(None, 
                        [service, _("paired just now"), hostname, port, service_name, None])
                return

            self.logging.info("connecting to %s:%s tcp to invite",
                    hostname, port)
            if self.inviting != None:
                self.inviting.window.destroy()
            self.inviting = Inviting(service, hostname, port, self)
            self.inviting.window.connect("destroy",
                    lambda *args: setattr(self, "inviting_window", None))

        tv_selection = tv.get_selection()
        tv_selection.connect("changed", clicked)

        scrolled_window = gtk.ScrolledWindow(hadjustment=None, vadjustment=None)
        scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        scrolled_window.set_border_width(10)
        scrolled_window.add_with_viewport(tv)
        scrolled_window.show()

        pick_box.pack_start(scrolled_window, True, False, 0)

        def add_service_to_list(name, description, host, port, version):
            """When a zeroconf service appears, this adds it to the
            listing of choices."""
            self.listening_hosts.append(None, [name, description, host, port, None])

        def remove_service_from_list(name):
            """When a zeroconf service disappears, this finds it in the 
            listing and removes it as an option for picking."""

            it = self.listening_hosts.get_iter_first()
            while it is not None:
                if self.listening_hosts.get_value(it, 0) == name:
                    self.listening_hosts.remove(it)
                    return True
                it = self.listening_hosts.iter_next(it)

        dbus_io.discover_services(add_service_to_list,
                remove_service_from_list, show_local=False)

        cell = gtk.CellRendererText()
        tv.append_column(hostname_col)
        hostname_col.pack_start(cell, True)
        hostname_col.add_attribute(cell, 'text', 2)

        cell = gtk.CellRendererText()
        cell.set_property("weight", pango.WEIGHT_BOLD)
        cell.set_property("weight-set", True)
        tv.append_column(hostid_col)
        hostid_col.pack_start(cell, True)
        hostid_col.add_attribute(cell, 'text', 0)

        cell = gtk.CellRendererText()
        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
        cell.set_property("ellipsize-set", True)
        tv.append_column(description_col)
        description_col.pack_start(cell, True)
        description_col.add_attribute(cell, 'text', 1)

        return pick_box

    def create_already_paired_pane(self, container):
        """Set up the pane that shows servers which are already paired."""

        # positions:        host id, descr, host, port, cloud_name, pairingid
        self.already_paired_hosts = gtk.TreeStore(str, str, str, int, str, str)

        import desktopcouch.replication_services as services
        for already_paired_record in couchdb_io.get_pairings():
            pid = already_paired_record.value["pairing_identifier"]
            if "service_name" in already_paired_record.value:
                srv_name = already_paired_record.value["service_name"]
                if srv_name.startswith("__"):
                    continue
                srv = getattr(services, srv_name)
                if not srv.is_active():
                    continue
                if already_paired_record.value.get("unpaired", False):
                    continue
                nice_description = _("paired ") + \
                        already_paired_record.value.get("ctime",
                                _("unknown date"))
                try:
                    self.already_paired_hosts.append(None, 
                            [srv.name, nice_description, "", 0, srv_name, pid])
                except Exception, e:
                    logging.error("Service %s had an error", srv_name, e)
            elif "server" in already_paired_record.value:
                hostname = already_paired_record.value["server"]
                nice_description = _("paired ") + \
                        already_paired_record.value.get("ctime",
                                _("unknown date"))
                self.already_paired_hosts.append(None, 
                        [hostname, nice_description, None, 0, None, pid])
            else:
                logging.error("unknown pairing record %s",
                        already_paired_record)

        hostid_col = gtk.TreeViewColumn(_("service name"))
        description_col = gtk.TreeViewColumn(_("service name"))

        pick_box = gtk.VBox()
        container.pack_start(pick_box, False, False, 10)

        l = gtk.Label(_("You're currently paired with these hosts.  Click to unpair."))
        pick_box.pack_start(l, False, False, 0)
        l.show()

        tv = gtk.TreeView(self.already_paired_hosts)
        tv.set_headers_visible(False)
        tv.set_rules_hint(True)
        tv.show()

        def clicked(selection):
            """An item in the list of services was clicked, so now we go 
            about inviting it to pair with us."""

            model, iter = selection.get_selected()
            if not iter:
                return
            service = model.get_value(iter, 0)
            hostname = model.get_value(iter, 2)
            port = model.get_value(iter, 3)
            service_name = model.get_value(iter, 4)
            pid = model.get_value(iter, 5)
            
            if service_name:
                # delete record
                for record in couchdb_io.get_pairings():
                    couchdb_io.remove_pairing(record.id, True)
                    
                # remove from already-paired list
                self.already_paired_hosts.remove(iter)
                # add to listening list
                srv = getattr(services, service_name)
                self.listening_hosts.append(None, [service, srv.description, 
                        hostname, port, service_name])
                return

            # delete (really, mark as "unpaired")
            for record in couchdb_io.get_pairings():
                if record.value["pairing_identifier"] == pid:
                    couchdb_io.remove_pairing(record.id, False)
                    break
                
            # remove from already-paired list
            self.already_paired_hosts.remove(iter)
            # do not add to listening list -- if it's listening then zeroconf
            # will pick it up
            return

        tv_selection = tv.get_selection()
        tv_selection.connect("changed", clicked)

        scrolled_window = gtk.ScrolledWindow(hadjustment=None, vadjustment=None)
        scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        scrolled_window.set_border_width(10)
        scrolled_window.add_with_viewport(tv)
        scrolled_window.show()

        pick_box.pack_start(scrolled_window, True, False, 0)

        cell = gtk.CellRendererText()
        tv.append_column(hostid_col)
        hostid_col.pack_start(cell, True)
        hostid_col.add_attribute(cell, 'text', 0)

        cell = gtk.CellRendererText()
        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
        cell.set_property("ellipsize-set", True)
        tv.append_column(description_col)
        description_col.pack_start(cell, True)
        description_col.add_attribute(cell, 'text', 1)

        return pick_box

    def create_single_listen_pane(self, container):
        """This sets up an "Alice" from the module's story.
        
        This assumes we're pairing a single, known local CouchDB instance,
        instead of generic instances that we'd need more information to talk
        about.  Instead of using this function, one might use another that
        lists local DBs as a way of picking one to pair.  This function assumes
        we know the answer to that."""

        def listen(btn, couchdb_instance=None):
            """When we decide to listen for invitations, this spawns the
            advertising/information window, and disables the button that
            causes/-ed us to get here (to prevent multiple instances)."""

            if couchdb_instance is None:
                # assume local for this user.  FIXME
                pass

            btn.set_sensitive(False)
            listening = Listening(couchdb_instance)
            listening.window.connect("destroy",
                    lambda w: btn.set_sensitive(True))

        padding = 6

        listen_box = gtk.HBox()
        container.pack_start(listen_box, False, False, 2)

        l = gtk.Label(_("Add this host to the list for others to see?"))
        listen_box.pack_start(l, True, False, padding)
        l.show()

        listen_button = gtk.Button(_("Listen for invitations"))
        listen_button.connect("clicked", listen, None)
        listen_box.pack_start(listen_button, True, False, padding)
        listen_button.show()

        return listen_box

    def create_any_listen_pane(self, container):
        """Unused.  An example of where to start when we don't already have
        a couchdb instance in mind to advertise.  This should be the generic
        version."""

        l = gtk.Label(_("I also know of CouchDB sessions here.  Pick one " +
                "to add it to the invitation list for other computers to see."))

        local_sharables_list = gtk.TreeStore(str, str)
        # et c, et c
        #some_row_in_list.connect("clicked", self.listen, target_db_info)

    def __init__(self):
        

        self.logging = logging.getLogger(self.__class__.__name__)

        self.window = gtk.Window()

        self.window.connect("delete_event", self.delete_event)
        self.window.connect("destroy", self.destroy)

        self.window.set_border_width(8)
        self.window.set_title(_("CouchDB Pairing Tool"))

        top_vbox = gtk.VBox()
        self.window.add(top_vbox)

        self.pick_pane = self.create_pick_pane(top_vbox)
        self.listen_pane = self.create_single_listen_pane(top_vbox)
        seperator = gtk.HSeparator()
        top_vbox.pack_start(seperator, False, False, 10)
        seperator.show()
        self.already_paired_pane = self.create_already_paired_pane(top_vbox)

        copyright = gtk.Label(_("Copyright 2009 Canonical"))
        top_vbox.pack_end(copyright, False, False, 0)
        copyright.show()

        self.pick_pane.show()
        self.listen_pane.show()
        self.already_paired_pane.show()

        top_vbox.show()
        self.window.show()


def pair_with_host(hostname, hostid, oauth_data, parent):
    """We've verified all is correct and authorized, so now we pair
    the databases."""
    logging.info("verified host %s/%s.  Done!", hostname, hostid)

    try:
        result = couchdb_io.put_dynamic_paired_host(hostname, hostid, oauth_data)
        assert result is not None
    except Exception, e:
        logging.exception("failure writing record for %s", hostname)
        fail_note = gtk.MessageDialog(
                parent=parent,
                flags=gtk.DIALOG_DESTROY_WITH_PARENT,
                buttons=gtk.BUTTONS_OK,
                type=gtk.MESSAGE_ERROR,
                message_format =_("Couldn't save pairing details for %s") % hostname)
        fail_note.run()
        fail_note.destroy()
        return
    
    success_note = gtk.Dialog(title=_("Paired with %(hostname)s") % locals(), 
            parent=parent,
            flags=gtk.DIALOG_DESTROY_WITH_PARENT,
            buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,))
    text = gtk.Label(
            _("Successfully paired with %(hostname)s.") % locals())
    text.show()
    content_box = success_note.get_content_area()
    content_box.pack_start(text, True, True, 20)
    success_note.connect("close",
            lambda *args: parent.destroy())
    success_note.connect("response",
            lambda *args: parent.destroy())
    success_note.show()


def pair_with_cloud_service(service_name, parent):
    """Write a paired server record for the selected cloud service."""
    try:
        import desktopcouch.replication_services as services
        srv = getattr(services, service_name)
        oauth_data = srv.get_oauth_data()
        result = couchdb_io.put_static_paired_service(oauth_data, service_name)
        assert result != None
    except Exception, e:
        logging.exception("failure in module for service %r", service_name)
        fail_note = gtk.MessageDialog(
                parent=parent,
                flags=gtk.DIALOG_DESTROY_WITH_PARENT,
                buttons=gtk.BUTTONS_OK,
                type=gtk.MESSAGE_ERROR,
                message_format =_("Couldn't save pairing details for %r") % service_name)
        fail_note.run()
        fail_note.destroy()
        return
    
    success_note = gtk.MessageDialog(
            parent=parent,
            flags=gtk.DIALOG_DESTROY_WITH_PARENT,
            buttons=gtk.BUTTONS_OK,
            type=gtk.MESSAGE_INFO,
            message_format =_("Successfully paired with %s") % service_name)
    success_note.run()
    success_note.destroy()


def set_couchdb_bind_address():
    from desktopcouch.records.server import CouchDatabase
    from desktopcouch import local_files
    bind_address = local_files.get_bind_address()

    if bind_address not in ("127.0.0.1", "0.0.0.0", "::1", None):
        logging.info("we're not qualified to change explicit address %s",
                bind_address)
        return False

    db = CouchDatabase("management", create=True)
    results = db.get_records(create_view=True)
    count = 0
    for row in results[pairing_record_type]:
        # Is the record of something that probably connects back to us?
        if "server" in row.value and row.value["server"] != "":
            count += 1
    couchdb_io.get_my_host_unique_id(create=True)  # ensure self-id record
    logging.debug("paired back-connecting machine count is %d", count)
    if count > 0:
        if ":" in bind_address:
            want_bind_address = "::0"  # IPv6 addr any
        else:
            want_bind_address = "0.0.0.0"
    else:
        if ":" in bind_address:
            want_bind_address = "::1"  # IPv6 loop back
        else:
            want_bind_address = "127.0.0.1"

    if bind_address != want_bind_address:
        local_files.set_bind_address(want_bind_address)
        logging.warning("changing the desktopcouch bind address from %r to %r",
                bind_address, want_bind_address)

def main(args):
    """Start execution."""
    import gobject
    gobject.set_application_name("desktopcouch pairing tool")

    logging.basicConfig(level=logging.DEBUG, format=
            "%(asctime)s [%(process)d] %(name)s:%(levelname)s:  %(message)s")

    gettext.install("couchdb_pairing")

    try:
        logging.debug("starting couchdb pairing tool")
        pick_or_listen = PickOrListen()
        return run_program()
    finally:
        set_couchdb_bind_address()
        logging.debug("exiting couchdb pairing tool")


if __name__ == "__main__":
    import sys
    import desktopcouch
    desktopcouch_port = desktopcouch.find_port()
    main(sys.argv)
