First pass of new structure. test.py is the only entrypoint which works as of this...
authorAndrew Flegg <andrew@bleb.org>
Sun, 10 Jan 2010 20:29:06 +0000 (20:29 +0000)
committerAndrew Flegg <andrew@bleb.org>
Sun, 10 Jan 2010 20:29:06 +0000 (20:29 +0000)
31 files changed:
package/src/console.py [deleted file]
package/src/contacts.py [deleted file]
package/src/contactview.py [deleted file]
package/src/gui.py [deleted file]
package/src/main.py [new file with mode: 0644]
package/src/mapcontact.py [deleted file]
package/src/names.py [deleted file]
package/src/org/__init__.py [new file with mode: 0644]
package/src/org/__init__.pyo [new file with mode: 0644]
package/src/org/bleb/__init__.py [new file with mode: 0644]
package/src/org/bleb/wimpworks.py [new file with mode: 0644]
package/src/org/maemo/__init__.py [new file with mode: 0644]
package/src/org/maemo/hermes/__init__.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/__init__.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/contact.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/facebook/__init__.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/facebook/provider.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/facebook/service.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/names.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/provider.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/service.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/twitter/__init__.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/twitter/provider.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/twitter/service.py [new file with mode: 0644]
package/src/org/maemo/hermes/gui/__init__.py [new file with mode: 0644]
package/src/org/maemo/hermes/gui/console.py [new file with mode: 0644]
package/src/org/maemo/hermes/gui/contactview.py [new file with mode: 0644]
package/src/org/maemo/hermes/gui/gtk.py [new file with mode: 0644]
package/src/org/maemo/hermes/gui/mapcontact.py [new file with mode: 0644]
package/src/test.py [new file with mode: 0644]
package/src/wimpworks.py [deleted file]

diff --git a/package/src/console.py b/package/src/console.py
deleted file mode 100644 (file)
index 8b4f887..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-class ConsoleUICallback:
-    """Meets the fb2contacts' authentication callback contract using
-       the console.
-    
-       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
-       Released under the Artistic Licence."""
-
-    # -----------------------------------------------------------------------
-    def need_auth(self):
-        print 'Need authentication...'
-
-    # -----------------------------------------------------------------------
-    def block_for_auth(self):
-        print 'Press enter when logged in...'
-        raw_input()
-    
-    # -----------------------------------------------------------------------
-    def progress(self, current, maximum):
-        print current, maximum
-
diff --git a/package/src/contacts.py b/package/src/contacts.py
deleted file mode 100644 (file)
index 9fa159f..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-import os
-import os.path
-import urllib
-import Image
-import ImageOps
-import StringIO
-import datetime
-import re
-from pygobject import *
-from ctypes import *
-
-# Constants from http://library.gnome.org/devel/libebook/stable/EContact.html#EContactField
-ebook = CDLL('libebook-1.2.so.5')
-E_CONTACT_HOMEPAGE_URL = 42
-E_CONTACT_PHOTO = 94
-E_CONTACT_BIRTHDAY_DATE = 107
-
-
-class ContactStore:
-    """Provide an API for changing contact data. Abstracts limitations
-       in the evolution-python bindings.
-
-       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
-       Released under the Artistic Licence."""
-
-
-    # -----------------------------------------------------------------------
-    def __init__(self, book):
-        """Create a new contact store for modifying contacts in the given
-           EBook."""
-        
-        self.book = book
-    
-    
-    # -----------------------------------------------------------------------
-    def close(self):
-        """Close the store and tidy-up any resources."""
-        
-        pass
-    
-    
-    # -----------------------------------------------------------------------
-    def set_photo(self, contact, url):
-        """Set the given contact's photo to the picture found at the URL. If the
-           photo is wider than it is tall, it will be cropped with a bias towards
-           the top of the photo."""
-        
-        f = urllib.urlopen(url)
-        data = ''
-        while True:
-            read_data = f.read()
-            data += read_data
-            if not read_data:
-                break
-        
-        im = Image.open(StringIO.StringIO(data))
-        (w, h) = im.size
-        if (h > w):
-            print "Shrinking photo for %s as it's %d x %d" % (contact.get_name(), w, h)
-            im = ImageOps.fit(im, (w, w), Image.NEAREST, 0, (0, 0.1))
-          
-        print "Updating photo for %s" % (contact.get_name())
-        f = StringIO.StringIO()
-        im.save(f, "JPEG")
-        image_data = f.getvalue()
-        photo = EContactPhoto()
-        photo.type = 0
-        photo.data = EContactPhoto_data()
-        photo.data.inlined = EContactPhoto_inlined()
-        photo.data.inlined.mime_type = cast(create_string_buffer("image/jpeg"), c_char_p)
-        photo.data.inlined.length = len(image_data)
-        photo.data.inlined.data = cast(create_string_buffer(image_data), c_void_p)
-        ebook.e_contact_set(hash(contact), E_CONTACT_PHOTO, addressof(photo))
-        return True
-      
-      
-    # -----------------------------------------------------------------------
-    def set_birthday(self, contact, day, month, year = 0):
-        if year == 0:
-            year = datetime.date.today().year
-          
-        birthday = EContactDate()
-        birthday.year = year
-        birthday.month = month
-        birthday.day = day
-        print "Setting birthday for [%s] to %d-%d-%d" % (contact.get_name(), year, month, day)
-        ebook.e_contact_set(hash(contact), E_CONTACT_BIRTHDAY_DATE, addressof(birthday))
-        return True
-      
-      
-    # -----------------------------------------------------------------------
-    def get_urls(self, contact):
-        """Return a list of URLs which are associated with this contact."""
-        
-        urls = []
-        ai = GList.new(ebook.e_contact_get_attributes(hash(contact), E_CONTACT_HOMEPAGE_URL))
-        while ai.has_next():
-            attr = ai.next(as_a = EVCardAttribute)
-            if not attr:
-                raise Exception("Unexpected null attribute for [" + contact.get_name() + "] with URLs " + urls)
-            urls.append(string_at(attr.value().next()))
-          
-        return urls
-    
-      
-    # -----------------------------------------------------------------------
-    def add_url(self, contact, str, unique = ''):
-        """Add a new URL to the set of URLs for the given contact."""
-        
-        urls = re.findall('(?:(?:ftp|https?):\/\/|\\bwww\.|\\bftp\.)[,\w\.\-\/@:%?&=%+#~_$\*]+[\w=\/&=+#]', str, re.I | re.S)
-        updated = False
-        for url in urls:
-            updated = self._add_url(contact, url, unique or re.sub('(?:.*://)?(\w+(?:[\w\.])*).*', '\\1', url)) or updated
-        
-        return updated
-    
-    
-    # -----------------------------------------------------------------------
-    def _add_url(self, contact, url, unique):
-        """Do the work of adding a unique URL to a contact."""
-        
-        url_attr = None
-        ai = GList.new(ebook.e_contact_get_attributes(hash(contact), E_CONTACT_HOMEPAGE_URL))
-        while ai.has_next():
-            attr = ai.next(as_a = EVCardAttribute)
-            existing = string_at(attr.value().next())
-            #print "Existing URL [%s] when adding [%s] to [%s] with constraint [%s]" % (existing, url, contact.get_name(), unique)
-            if existing == unique or existing == url:
-                return False
-            elif existing.find(unique) > -1:
-                url_attr = attr
-          
-        if not url_attr:
-            ai.add()
-            url_attr = EVCardAttribute()
-            url_attr.group = ''
-            url_attr.name = 'URL'
-        
-        val = GList()
-        print "Setting URL for [%s] to [%s]" % (contact.get_name(), url)
-        val.set(create_string_buffer(url))
-        ai.set(addressof(url_attr))
-        url_attr.values = cast(addressof(val), POINTER(GList))
-        ebook.e_contact_set_attributes(hash(contact), E_CONTACT_HOMEPAGE_URL, addressof(ai))
-        return True
-
diff --git a/package/src/contactview.py b/package/src/contactview.py
deleted file mode 100644 (file)
index 569bb91..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-import gtk
-import hildon
-import gobject
-import evolution
-from ctypes import *
-from pygobject import *
-
-class ContactView(hildon.PannableArea):
-    """Widget which shows a list of contacts in a pannable area.
-         
-       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
-       Released under the Artistic Licence."""
-    
-    
-    # -----------------------------------------------------------------------
-    def __init__(self, contacts):
-        """Constructor. Passed a list of EContacts."""
-        
-        hildon.PannableArea.__init__(self)
-        self.contacts = contacts
-        self.treestore = gtk.ListStore(str, gtk.gdk.Pixbuf, gobject.TYPE_PYOBJECT)
-        for contact in self.contacts:
-            if not contact.get_name():
-                continue
-              
-            photo = contact.get_property('photo')
-            pi = cast(c_void_p(hash(photo)), POINTER(EContactPhoto))
-            pixbuf = None
-            if pi.contents.data.uri.startswith("image/"):
-                data = string_at(pi.contents.data.inlined.data, pi.contents.data.inlined.length)
-                pixbuf_loader = gtk.gdk.PixbufLoader()
-                pixbuf_loader.write(data)
-                pixbuf_loader.close()
-                pixbuf = pixbuf_loader.get_pixbuf()
-            elif pi.contents.data.uri.startswith("file://"):
-                filename = pi.contents.data.uri[7:]
-                pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
-                  
-            if pixbuf:
-                size = min(pixbuf.get_width(), pixbuf.get_height())
-                pixbuf = pixbuf.subpixbuf(0, 0, size, size).scale_simple(48, 48, gtk.gdk.INTERP_BILINEAR)
-            self.treestore.append(row = [contact.get_name(), pixbuf, contact])
-        
-        self.treeview = gtk.TreeView(self.treestore)
-        tvcolumn = gtk.TreeViewColumn('Name', gtk.CellRendererText(), text = 0)
-        self.treeview.append_column(tvcolumn)
-        
-        cell = gtk.CellRendererPixbuf()
-        cell.set_property('xalign', 1.0)
-        tvcolumn = gtk.TreeViewColumn('Picture', cell, pixbuf = 1)
-        self.treeview.append_column(tvcolumn)
-        
-        self.treeview.set_search_column(0)
-        self.treeview.connect('row-activated', self._activated)
-        self.add(self.treeview)
-        self.set_size_request(600, 380)
-      
-      
-    # -----------------------------------------------------------------------
-    def _activated(self, treeview, path, column):
-        """Used to emit the `contact-activated' signal once a row has been
-           selected."""
-        
-        iter    = treeview.get_model().get_iter(path)
-        contact = treeview.get_model().get_value(iter, 2)
-        self.emit('contact-activated', contact)
-
-
-_contact_activated = gobject.signal_new('contact-activated', ContactView, gobject.SIGNAL_ACTION, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT])
-
-
-
diff --git a/package/src/gui.py b/package/src/gui.py
deleted file mode 100755 (executable)
index e558f44..0000000
+++ /dev/null
@@ -1,293 +0,0 @@
-#!/usr/bin/env python
-
-import gettext
-import gtk, gobject
-import traceback
-import time
-import thread
-import contactview
-import urllib2
-import hildon
-import wimpworks
-import mapcontact
-from wimpworks import WimpWorks
-from hermes import Hermes
-
-class HermesGUI(WimpWorks):
-    """Provides the GUI for Hermes, allowing the syncing of Facebook and
-       Twitter friends' information with the Evolution contacts' database.
-       
-       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
-       Released under the Artistic Licence."""
-
-
-    # -----------------------------------------------------------------------
-    def __init__(self):
-        gettext.install('hermes','/opt/hermes/share/locale/')
-        WimpWorks.__init__(self, 'Hermes', version = '0.2.0', dbus_name = 'org.maemo.hermes')
-        self.set_background('background.png')
-        
-        layout = wimpworks.HildonMainScreenLayout(offset = 0.8, container = self)
-        layout.add_button('Retrieve', _("Get contacts' missing info"))
-        layout.add_button('Refresh', _("Update contacts' info"))
-        
-        self.add_menu_action("Accounts")
-        self.menu.show_all()
-
-  
-    # -----------------------------------------------------------------------
-    def do_retrieve(self, widget):
-        self.sync(widget, False)
-    
-    
-    # -----------------------------------------------------------------------
-    def do_refresh(self, widget):
-        self.sync(widget, True)
-
-
-    # -----------------------------------------------------------------------
-    def do_accounts(self, widget = None):
-        dialog = gtk.Dialog(_('Accounts'), self.main_window)
-        dialog.add_button(_('Save'), gtk.RESPONSE_OK)
-        
-        #pa = hildon.PannableArea()
-        #dialog.vbox.add(pa)
-        content = dialog.vbox 
-        #content = gtk.VBox()
-        #pa.add(content)
-        #pa.set_size_request(600, 380)
-        
-        use_facebook = self.new_checkbox(_('Use Facebook'), content)
-        use_facebook.set_active(self.get_use_facebook())
-        
-        indent = self.new_indent(content)
-        self.link_control(use_facebook, gtk.Label(_('Note: authentication via web page')), indent)
-        
-        fb_empty = self.link_control(use_facebook, self.new_checkbox(_('Create birthday-only contacts')), indent)
-        fb_empty.set_active(self.get_create_empty())
-        
-        use_twitter = self.new_checkbox(_('Use Twitter'), content)
-        use_twitter.set_active(self.get_use_twitter())
-        
-        indent = self.new_indent(content)
-        tw_user = self.link_control(use_twitter, self.new_input(_('Twitter username')), indent)
-        tw_user.set_text(self.get_twitter_credentials()[0])
-        
-        tw_pass = self.link_control(use_twitter, self.new_input(_('Twitter password'), password = True), indent)
-        tw_pass.set_text(self.get_twitter_credentials()[1])
-        
-        dialog.show_all()
-        result = dialog.run()
-        dialog.hide()
-        if result == gtk.RESPONSE_OK:
-            self.set_use_facebook(use_facebook.get_active())
-            self.set_create_empty(fb_empty.get_active())
-            self.set_use_twitter(use_twitter.get_active(), tw_user.get_text(), tw_pass.get_text())
-        
-        return result
-   
-
-    # -----------------------------------------------------------------------
-    def sync(self, widget, force, main = True):
-        if main and not self.get_use_facebook() and not self.get_use_twitter():
-            saved = self.do_accounts()
-            if saved == gtk.RESPONSE_DELETE_EVENT:
-                return
-        
-        if main:
-            self.main_window.set_property('sensitive', False)
-            thread.start_new_thread(self.sync, (widget, force, False))
-        else:
-            try:
-                fb2c = Hermes(self,
-                              twitter = (self.get_use_twitter() and self.get_twitter_credentials()) or None,
-                              facebook = self.get_use_facebook(),
-                              empty = self.get_create_empty())
-                fb2c.load_friends()
-                fb2c.sync_contacts(resync = force)
-                gobject.idle_add(self.open_summary, fb2c)
-        
-            except urllib2.HTTPError, e:
-                traceback.print_exc()
-                if e.code == 401:
-                    gobject.idle_add(self.report_error, _('Authentication problem. Check credentials.'), True)
-                else:
-                    gobject.idle_add(self.report_error, _('Network connection error. Check connectivity.'))
-        
-            except urllib2.URLError, e:
-                traceback.print_exc()
-                gobject.idle_add(self.report_error, _('Network connection error. Check connectivity.'))
-          
-            except Exception, e:
-                traceback.print_exc()
-                gobject.idle_add(self.report_error, _('Something went wrong: ') + e.message)
-    
-    
-    # -----------------------------------------------------------------------
-    def open_summary(self, fb2c):
-        gobject.idle_add(self.main_window.set_property, 'sensitive', True)
-    
-        dialog = gtk.Dialog(_('Summary'), self.main_window)
-        dialog.add_button(_('Done'), gtk.RESPONSE_OK)
-      
-        button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
-                               title = _('Updated %d contacts') % (len(fb2c.updated)))
-        button.connect('clicked', self.show_contacts, fb2c, fb2c.updated)
-        button.set_property('sensitive', len(fb2c.updated) > 0)
-        dialog.vbox.add(button)
-        
-        button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
-                               title = _('Matched %d contacts') % (len(fb2c.matched)))
-        button.connect('clicked', self.show_contacts, fb2c, fb2c.matched)
-        button.set_property('sensitive', len(fb2c.matched) > 0)
-        dialog.vbox.add(button)
-      
-        button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
-                               title = _('%d contacts unmatched') % (len(fb2c.unmatched)))
-        button.connect('clicked', self.show_contacts, fb2c, fb2c.unmatched)
-        button.set_property('sensitive', len(fb2c.unmatched) > 0)
-        dialog.vbox.add(button)
-      
-        dialog.show_all()
-        dialog.run()
-        dialog.hide()
-    
-    
-    # -----------------------------------------------------------------------
-    def show_contacts(self, widget, fb2c, contacts):
-        view = contactview.ContactView(contacts)
-    
-        dialog = gtk.Dialog(_('Contacts'), self.main_window)
-        view.connect('contact-activated', self.map_contact, fb2c)
-        dialog.vbox.add(view)
-        dialog.show_all()
-      
-        dialog.run()
-        dialog.hide()
-      
-      
-    # -----------------------------------------------------------------------
-    def map_contact(self, widget, contact, fb2c):
-        view = mapcontact.MapContact(fb2c.friends, contact)
-    
-        dialog = gtk.Dialog(contact.get_name(), self.main_window)
-        dialog.add_button(_('Update'), gtk.RESPONSE_OK)
-        dialog.vbox.add(view)
-        dialog.show_all()
-      
-        result = dialog.run()
-        dialog.hide()
-        if result == gtk.RESPONSE_OK:
-            friend = view.get_selected_friend()
-            if friend:
-                if 'contact' in friend and friend['contact'] == contact:
-                    hildon.hildon_banner_show_information(self.main_window, '', _("Removing existing mappings is not yet supported"))
-                elif view.contact_mapped:
-                    if fb2c.update_contact(contact, friend, True):
-                        fb2c.addresses.commit_contact(contact)
-                else:
-                    if fb2c.update_contact(contact, friend, False):
-                        fb2c.addresses.commit_contact(contact)
-    
-      
-    # -----------------------------------------------------------------------
-    def need_auth(self, main = False):
-        if main:
-            hildon.hildon_banner_show_information(self.main_window, '', _("Need to authenticate with Facebook"))
-        else:
-            gobject.idle_add(self.need_auth, True)
-      
-    
-    # -----------------------------------------------------------------------
-    def block_for_auth(self, main = False, lock = None):
-        if main:
-            note = gtk.Dialog(_('Facebook authorisation'), self.main_window)
-            note.add_button(_("Validate"), gtk.RESPONSE_OK)
-            note.vbox.add(gtk.Label(_("\nPress 'Validate' once Facebook has\nbeen authenticated in web browser.\n")))
-    
-            note.show_all()
-            result = note.run()
-            note.hide()
-            lock.release()
-        
-        else:
-            time.sleep(2)
-            lock = thread.allocate_lock()
-            lock.acquire()
-            gobject.idle_add(self.block_for_auth, True, lock)
-            lock.acquire()
-            lock.release()
-    
-    
-    # -----------------------------------------------------------------------
-    def progress(self, i, j, main = False):
-        if main:
-            if i == 0:
-                self.progressbar = gtk.ProgressBar()
-                self.progressnote = gtk.Dialog(_("Fetching friends' info"), self.main_window)
-                self.progressnote.vbox.add(self.progressbar)
-                hildon.hildon_gtk_window_set_progress_indicator(self.progressnote, 1)
-               
-                self.progressnote.show_all()
-              
-            elif i < j:
-                if i == 1:
-                    self.progressnote.set_title(_("Updating contacts"))
-                    hildon.hildon_gtk_window_set_progress_indicator(self.progressnote, 0)
-                
-                self.progressbar.set_fraction(float(i) / float(j))
-              
-            else:
-                self.progressnote.destroy()
-              
-            print i,j
-        else:
-            gobject.idle_add(self.progress, i, j, True)
-    
-    
-    # -----------------------------------------------------------------------
-    def report_error(self, e, prefs = False):
-        if self.progressnote:
-            self.main_window.set_property('sensitive', True)
-            self.progressnote.destroy()
-    
-        hildon.hildon_banner_show_information(self.main_window, '', e)
-        if prefs:
-            self.do_accounts()
-    
-        
-    def get_use_facebook(self):
-        return self.gconf.get_bool("/apps/maemo/hermes/use_facebook")
-    
-    
-    def set_use_facebook(self, value):
-        self.gconf.set_bool("/apps/maemo/hermes/use_facebook", value)
-    
-    def get_create_empty(self):
-        return self.gconf.get_bool("/apps/maemo/hermes/create_empty")
-    
-    
-    def set_create_empty(self, value):
-        self.gconf.set_bool("/apps/maemo/hermes/create_empty", value)
-    
-    
-    def get_use_twitter(self):
-        return self.gconf.get_bool("/apps/maemo/hermes/use_twitter")
-    
-    
-    def set_use_twitter(self, value, user, password):
-        self.gconf.set_bool("/apps/maemo/hermes/use_twitter", value)
-        self.gconf.set_string("/apps/maemo/hermes/twitter_user", user)
-        self.gconf.set_string("/apps/maemo/hermes/twitter_pwd", password)
-      
-    
-    def get_twitter_credentials(self):
-        return (self.gconf.get_string("/apps/maemo/hermes/twitter_user") or '',
-                self.gconf.get_string("/apps/maemo/hermes/twitter_pwd") or '')
-
-
-# -------------------------------------------------------------------------
-if __name__ == '__main__':
-    gui = HermesGUI()
-    gui.run()
-
diff --git a/package/src/main.py b/package/src/main.py
new file mode 100644 (file)
index 0000000..28484e6
--- /dev/null
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+
+from org.maemo.hermes.gui import HermesGUI
+gui = HermesGUI()
+gui.run()
+
diff --git a/package/src/mapcontact.py b/package/src/mapcontact.py
deleted file mode 100644 (file)
index ca668cd..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-import gtk
-import hildon
-from ctypes import *
-from pygobject import *
-
-class MapContact(hildon.PannableArea):
-    """Widget which shows a list of friends from various feeds and allows
-       the mapping to a particular contact.
-       
-       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
-       Released under the Artistic Licence."""
-
-
-    # -----------------------------------------------------------------------
-    def __init__(self, friends, contact):
-        """Constructor. Passed a list of `friends' and the contact we're mapping."""
-        
-        hildon.PannableArea.__init__(self)
-        self.friends = friends
-        self.contact = contact
-        self.treestore = gtk.ListStore(gtk.gdk.Pixbuf, str, gtk.gdk.Pixbuf, gobject.TYPE_PYOBJECT)
-        
-        accounts = {}
-        _facebook = gtk.gdk.pixbuf_new_from_file('/opt/hermes/share/account-facebook.png')
-        _twitter  = gtk.gdk.pixbuf_new_from_file('/opt/hermes/share/account-twitter.png')
-        _tick     = gtk.icon_theme_get_default().load_icon('widgets_tickmark_list', 48, 0)
-        
-        self.contact_mapped = False
-        mapped_iter = None
-        for key in sorted(self.friends.keys(), cmp = lambda a, b: cmp(a.lower(), b.lower())):
-            friend = self.friends[key]
-            if friend['account'] not in accounts:
-                accounts[friend['account']] = gtk.gdk.pixbuf_new_from_file("/opt/hermes/share/account-%s.png" % (friend['account']))
-              
-            photo = friend['pic']
-            pixbuf = None
-            if 'contact' in friend:
-                if friend['contact'] == contact:
-                    pixbuf = _tick
-                    self.contact_mapped = True
-                    mapped_iter = self.treestore.append([accounts[friend['account']], friend['name'], pixbuf, friend])
-                else:
-                    continue
-            else:
-                self.treestore.append([accounts[friend['account']], friend['name'], pixbuf, friend])
-        
-        self.treeview = gtk.TreeView(self.treestore)
-        hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
-        
-        self.treeview.append_column(gtk.TreeViewColumn('Account', gtk.CellRendererPixbuf(), pixbuf = 0))
-        self.treeview.append_column(gtk.TreeViewColumn('Name', gtk.CellRendererText(), text = 1))
-        
-        cell = gtk.CellRendererPixbuf()
-        cell.set_property('xalign', 1.0)
-        self.treeview.append_column(gtk.TreeViewColumn('Picture', cell, pixbuf = 2))
-        
-        if mapped_iter:
-            path = self.treestore.get_path(mapped_iter)
-            self.treeview.get_selection().select_path(path)
-            self.treeview.scroll_to_cell(path)
-          
-        self.add(self.treeview)
-        self.set_size_request(600, 320)
-      
-    # -----------------------------------------------------------------------
-    def get_selected_friend(self):
-        """Return the selected friend, or `None' if none."""
-    
-        (model, iter) = self.treeview.get_selection().get_selected()
-        if not iter:
-            return None
-             
-        return model.get_value(iter, 3)
-
-
-_account_selected = gobject.signal_new('account-selected', MapContact, gobject.SIGNAL_ACTION, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])
-
diff --git a/package/src/names.py b/package/src/names.py
deleted file mode 100644 (file)
index 59d5a0a..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-"""Utilities for determing name variants.
-
-   Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
-   Released under the Artistic Licence."""
-
-__names__ = [
-    ['Andrew', 'Andy', 'Andi', 'Drew'],
-    ['Christian', 'Chris'],
-    ['Christopher', 'Chris'],
-    ['David', 'Dave'],
-    ['Daniel', 'Dan', 'Danny'],
-    ['Michael', 'Mike', 'Mic', 'Mik', 'Micky'],
-    ['Peter', 'Pete'],
-    ['Robert', 'Rob', 'Bob', 'Bobby', 'Robbie'],
-  ]
-
-__map__ = {}
-for row in __names__:
-    for name in row:
-        if (not name in __map__):
-            __map__[name] = set(row)
-        else:
-            __map__[name] = __map__[name].union(row)
-
-# -----------------------------------------------------------------------
-def variants(name):
-    """Return a set of names which should be checked for given the input
-       name. Any word which is has a replacement will be replaced, and an
-       iterable list of all variants will be returned."""
-
-    result = set()
-    if (name is None):
-        return result
-    
-    name = unicode(name).encode('trans')
-    result.add(name)
-    bits = name.split(' ')
-    for bit in bits:
-        if (bit in __map__):
-            for replacement in __map__[bit]:
-                result.add(name.replace(bit, replacement))
-
-    return result
-
diff --git a/package/src/org/__init__.py b/package/src/org/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/package/src/org/__init__.pyo b/package/src/org/__init__.pyo
new file mode 100644 (file)
index 0000000..fbd458b
Binary files /dev/null and b/package/src/org/__init__.pyo differ
diff --git a/package/src/org/bleb/__init__.py b/package/src/org/bleb/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/package/src/org/bleb/wimpworks.py b/package/src/org/bleb/wimpworks.py
new file mode 100644 (file)
index 0000000..4f602f2
--- /dev/null
@@ -0,0 +1,284 @@
+#
+# WimpWorks (for Python)                (c) Andrew Flegg 2009.
+# ~~~~~~~~~~~~~~~~~~~~~~                Released under the Artistic Licence.
+#                                       http://www.bleb.org/
+
+import gettext
+import gtk, gobject
+import re
+import thread
+import os.path
+
+# -- Work out environment...
+#
+try:
+    import hildon
+    _have_hildon = True
+except ImportError:
+    _have_hildon = False
+  
+try:
+    import osso
+    _have_osso = True
+except ImportError:
+    _have_osso = False
+  
+try:
+    import gnome.gconf
+    _have_gconf = True
+except ImportError:
+    _have_gconf = False
+
+gobject.threads_init()
+
+# -- Main class...
+#
+class WimpWorks:
+    '''A framework for creating easy-to-use graphical user interfaces using
+       GTK+, Python, DBus and more.
+       
+       This is the base class. It should be constructed with a DBus name
+       and a version.
+         
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
+       Released under the Artistic Licence.'''
+    
+    
+    # -----------------------------------------------------------------------
+    def __init__(self, application, version = '1.0.0', dbus_name = None):
+        '''Constructor. Initialises the gconf connection, DBus, OSSO and more.
+        
+           @param application User-facing name of the application.
+           @param version Version string of the application.
+           @param dbus_name Name to register with DBus. If unspecified, no
+                  DBus registration will be performed.'''
+        
+        self.name = application
+        self.dbus_name = dbus_name
+        self.menu = None
+        
+        if _have_gconf:
+            self.gconf = gnome.gconf.client_get_default()
+        
+        if _have_hildon:
+            self.app = hildon.Program()
+            self.main_window = hildon.Window()
+            gtk.set_application_name(application)
+        else:
+            self.app = None
+            self.main_window = gtk.Window()
+        
+        self.main_window.set_title(application)
+        self.main_window.connect("delete-event", gtk.main_quit)
+        
+        if _have_osso and dbus_name:
+            self.osso_context = osso.Context(dbus_name, version, False)
+          
+        if self.app:
+            self.app.add_window(self.main_window)
+          
+        if _have_hildon:
+            self._expose_hid = self.main_window.connect('expose-event', self._take_screenshot)
+    
+        
+    # -----------------------------------------------------------------------
+    def set_background(self, file, window = None):
+        '''Set the background of the given (or main) window to that contained in
+           'file'.
+           
+           @param file File name to set. If not an absolute path, typical application
+                       directories will be checked.
+           @param window Window to set background of. If unset, will default to the
+                         main application window.'''
+        
+        # TODO Handle other forms of path
+        file = "/opt/%s/share/%s" % (re.sub('[^a-z0-9_]', '', self.name.lower()), file)
+        if not window:
+            window = self.main_window
+          
+        self._background, mask = gtk.gdk.pixbuf_new_from_file(file).render_pixmap_and_mask()
+        window.realize()
+        window.window.set_back_pixmap(self._background, False)
+    
+      
+    # -----------------------------------------------------------------------
+    def add_menu_action(self, title, window = None):
+        '''Add a menu action to the given (or main) window. Once add_menu_action()
+           has been called with all the properties, 'self.menu.show_all()' should be
+           called.
+        
+           @param title The label of the action, and used to compute the callback
+                        method. This should be the UN-i18n version: gettext is used
+                        on the value.'''
+        
+        if not window:
+            window = self.main_window
+          
+        if not self.menu:
+            if _have_hildon:
+                self.menu = hildon.AppMenu()
+                window.set_app_menu(self.menu)
+            else:
+                raise Exception("Menu needs to be created, and no Hildon present")
+            
+        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        button.set_label(_(title))
+        button.connect("clicked", self.callback, title)
+        self.menu.append(button)
+    
+    
+    # -----------------------------------------------------------------------
+    def run(self):
+        '''Once the application has been initialised, this will show the main window
+           and run the mainloop.'''
+         
+        self.main_window.show_all()
+        gtk.main()
+    
+    
+    # -----------------------------------------------------------------------
+    def _take_screenshot(self, event = None, data = None):
+        '''Used to provide a quick-loading screen.
+        
+           @see http://maemo.org/api_refs/5.0/5.0-final/hildon/hildon-Additions-to-GTK+.html#hildon-gtk-window-take-screenshot'''
+        
+        self.main_window.disconnect(self._expose_hid)
+        if not os.path.isfile("/home/user/.cache/launch/%s.pvr" % (self.dbus_name)):
+            gobject.timeout_add(80, hildon.hildon_gtk_window_take_screenshot, self.main_window, True)
+    
+      
+    # -----------------------------------------------------------------------
+    def callback(self, event, method):
+        '''Call a method on this object, using the given string to derive
+           the name. If no method is found, no action is taken.
+           
+           @param event Event which triggered the callback.
+           @param method String which will be lowercased to form a method
+                  called 'do_method'.'''
+        
+        method = re.sub('[^a-z0-9_]', '', method.lower())
+        getattr(self, "do_%s" % (method))(event.window)
+    
+      
+    # -----------------------------------------------------------------------
+    def new_checkbox(self, label, box = None):
+        '''Create a new checkbox, adding it to the given container.
+        
+           @param label Label for the checkbox.
+           @param box Optional container to add the created checkbox to.
+           @return The newly created checkbox.'''
+           
+        checkbox = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+        checkbox.set_label(label)
+        if box:
+            box.add(checkbox)
+        return checkbox
+    
+    
+    # -----------------------------------------------------------------------
+    def new_indent(self, box):
+        '''Create an indent which can be used to show items related to each other.
+        
+           @param box Container to add the indent to.'''
+           
+        outer = gtk.HBox()
+        indent = gtk.VBox()
+        outer.pack_start(indent, padding=48)
+        box.add(outer)
+        return indent
+    
+    
+    # -----------------------------------------------------------------------
+    def new_input(self, label, box = None, password = False):
+        '''Create a new input with the given label, optionally adding it to a
+           container.
+           
+           @param label Text describing the purpose of the input field.
+           @param box Optional container to add the input to.
+           @param password Boolean indicating if the input is used for passwords.
+           @return The newly created input.'''
+           
+        input = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+        input.set_placeholder(label)
+        input.set_property('is-focus', False)
+        
+        if password:
+            input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL | gtk.HILDON_GTK_INPUT_MODE_INVISIBLE)
+        else:
+            input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL)
+          
+        if box:
+            box.add(input)
+        return input
+    
+    
+    # -----------------------------------------------------------------------
+    def link_control(self, checkbox, ctrl, box = None):
+        '''Link a checkbox to a control, such that the editability of the
+           control is determined by the checkbox state.
+           
+           @param checkbox Checkbox which will control the state.
+           @param ctrl Control to add.
+           @param box Optional container to add 'ctrl' to.
+           @return The added control.'''
+           
+        if box:
+            box.add(ctrl)
+          
+        self._sync_edit(checkbox, ctrl)
+        checkbox.connect('toggled', self._sync_edit, ctrl)
+        return ctrl
+    
+      
+    # -----------------------------------------------------------------------
+    def _sync_edit(self, checkbox, edit):
+        edit.set_property('sensitive', checkbox.get_active())
+    
+
+      
+# -----------------------------------------------------------------------
+class HildonMainScreenLayout():
+    '''Provides a mechanism for creating a traditional multi-button button
+       selection, as made popular by Maemo 5's Media Player, Clock, Application
+       Manager and HIG.
+       
+       This does *not* require Hildon, however.
+    '''
+
+    # ---------------------------------------------------------------------
+    def __init__(self, container, offset = 0.5):
+        '''Create a new layout.
+        
+           @param container Container to add layout to. If unspecified,
+                  the application's main window will be used.
+           @param offset The vertical offset for the buttons. If unspecified,
+                  they will be centred. Ranges from 0.0 (top) to 1.0 (bottom).'''
+                  
+        self._container = container
+        alignment = gtk.Alignment(xalign=0.5, yalign=0.8, xscale=0.8)
+        self._box = gtk.HButtonBox()
+        alignment.add(self._box)
+        container.main_window.add(alignment)
+        self._box.set_property('layout-style', gtk.BUTTONBOX_SPREAD)
+
+      
+    # ---------------------------------------------------------------------
+    def add_button(self, title, subtitle = ''):
+        '''Add a button to the layout with the specified title. Upon clicking
+           the button, a method of the name 'do_title' will be invoked on the
+           main class.
+           
+           @param title Value of the button, and used to derive the callback method. This
+                        should be the UN-i18n version: gettext is used on the value.
+           @param subtitle An optional subtitle containing more information.'''
+        
+        if _have_hildon:         
+            button = hildon.Button(gtk.HILDON_SIZE_THUMB_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
+                                 title = title, value = subtitle)
+        else:
+            button = gtk.Button(label = _(title))
+        
+        button.set_property('width-request', 250)
+        button.connect('clicked', self._container.callback, title)
+        self._box.add(button)
+
diff --git a/package/src/org/maemo/__init__.py b/package/src/org/maemo/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/package/src/org/maemo/hermes/__init__.py b/package/src/org/maemo/hermes/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/package/src/org/maemo/hermes/engine/__init__.py b/package/src/org/maemo/hermes/engine/__init__.py
new file mode 100644 (file)
index 0000000..5067091
--- /dev/null
@@ -0,0 +1,52 @@
+class Hermes:
+    """Encapsulate the process of syncing online friends' information with the
+       Evolution contacts' database. This should be used as follows:
+       
+         * Initialise, passing in a GUI callback.
+         * Call initialise_services().
+         * Call sync_contacts().
+         * Retrieve information on changes effected.
+         * Call update_contact to enact manual mapping.
+         
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
+       Released under the Artistic Licence."""
+    
+    
+    # -----------------------------------------------------------------------
+    def __init__(self, gui_callback):
+        """Constructor. Passed a callback which must implement three informational
+           methods:
+           
+             need_auth() - called to indicate an external login is about to occur.
+                           The user should be informed.
+                           
+             block_for_auth() - prompt the user to take some action once they have
+                                successfully logged in to Facebook.
+                              
+             progress(i, j) - the application is currently processing friend 'i' of
+                              'j'. Should be used to provide the user a progress bar.
+        """
+        
+        pass
+"""        
+ friends = ()
+ for service in services:
+   for friend in service.get_friends():
+     friends.add(friend)
+
+ all_contacts = get_contacts_as_set()
+ contacts = set()
+ updated_contacts = set()
+ for econtact in addressbook.get_all_contacts():
+   contact = Contact(addressbook, econtact)
+   contacts.add(contact)
+   for service in something.get_services_by_prioritisation():
+     if service.process_contact(contact):
+       updated_contacts.add(contact)
+
+ for service in something.get_services_by_prioritisation():
+   service.finalise(updated_contacts)
+
+ for contact in updated_contacts:
+   contact.save()
+"""
\ No newline at end of file
diff --git a/package/src/org/maemo/hermes/engine/contact.py b/package/src/org/maemo/hermes/engine/contact.py
new file mode 100644 (file)
index 0000000..8d4a367
--- /dev/null
@@ -0,0 +1,198 @@
+import urllib
+import Image
+import ImageOps
+import StringIO
+import datetime
+import re
+from org.maemo.hermes.engine.names import canonical, variants
+from pygobject import *
+from ctypes import *
+
+# Constants from http://library.gnome.org/devel/libebook/stable/EContact.html#EContactField
+ebook = CDLL('libebook-1.2.so.5')
+E_CONTACT_HOMEPAGE_URL = 42
+E_CONTACT_PHOTO = 94
+E_CONTACT_EMAIL = 97
+E_CONTACT_BIRTHDAY_DATE = 107
+
+
+class Contact:
+    """Provide an abstraction of contact, working around limitations
+       in the evolution-python bindings. Properties are defined at:
+       
+          http://library.gnome.org/devel/libebook/stable/EContact.html#id3066469
+
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
+       Released under the Artistic Licence."""
+
+       
+    # -----------------------------------------------------------------------
+    def __init__(self, book, contact):
+        """Create a new contact store for modifying contacts in the given
+           EBook."""
+        
+        self._book = book
+        self._contact = contact
+        self._identifiers = self._find_ids()
+        
+        
+    # -----------------------------------------------------------------------
+    def _find_ids(self):
+        """Return a set of the identifiers which should be used to match this
+           contact. Includes variants of first name, last name, nickname and
+           email address. These are all Unicode-normalised into the traditional
+           US-ASCII namespace"""
+           
+        result = set()
+        for name in variants(self._contact.get_name()):
+            result.add(canonical(name))
+            
+        for name in variants(self._contact.get_property('nickname')):
+            result.add(canonical(name))
+            
+        for email in self.get_emails():
+            user = canonical(email.split('@')[0])
+            if len(user) > 4:
+                result.add(user)
+
+        return result
+    
+    
+    # -----------------------------------------------------------------------
+    def get_name(self):
+        """Return this contact's name."""
+        
+        return self._contact.get_name() or self._contact.get_property('nickname')
+    
+    
+    # -----------------------------------------------------------------------
+    def get_identifiers(self):
+        """Return the lowercase, Unicode-normalised, all-alphabetic
+           versions of identifiers for this contact."""
+           
+        return self._identifiers
+    
+    
+    # -----------------------------------------------------------------------
+    def set_photo(self, url):
+        """Set the given contact's photo to the picture found at the URL. If the
+           photo is wider than it is tall, it will be cropped with a bias towards
+           the top of the photo."""
+        
+        f = urllib.urlopen(url)
+        data = ''
+        while True:
+            read_data = f.read()
+            data += read_data
+            if not read_data:
+                break
+        
+        im = Image.open(StringIO.StringIO(data))
+        (w, h) = im.size
+        if (h > w):
+            print "Shrinking photo for %s as it's %d x %d" % (self._contact.get_name(), w, h)
+            im = ImageOps.fit(im, (w, w), Image.NEAREST, 0, (0, 0.1))
+          
+        print "Updating photo for %s" % (self._contact.get_name())
+        f = StringIO.StringIO()
+        im.save(f, "JPEG")
+        image_data = f.getvalue()
+        photo = EContactPhoto()
+        photo.type = 0
+        photo.data = EContactPhoto_data()
+        photo.data.inlined = EContactPhoto_inlined()
+        photo.data.inlined.mime_type = cast(create_string_buffer("image/jpeg"), c_char_p)
+        photo.data.inlined.length = len(image_data)
+        photo.data.inlined.data = cast(create_string_buffer(image_data), c_void_p)
+        ebook.e_contact_set(hash(self._contact), E_CONTACT_PHOTO, addressof(photo))
+        return True
+      
+      
+    # -----------------------------------------------------------------------
+    def set_birthday(self, day, month, year = 0):
+        """Set the birthday for this contact to the given day, month and year."""
+        
+        if year == 0:
+            year = datetime.date.today().year
+          
+        birthday = EContactDate()
+        birthday.year = year
+        birthday.month = month
+        birthday.day = day
+        print "Setting birthday for [%s] to %d-%d-%d" % (self._contact.get_name(), year, month, day)
+        ebook.e_contact_set(hash(self._contact), E_CONTACT_BIRTHDAY_DATE, addressof(birthday))
+        return True
+    
+    
+    # -----------------------------------------------------------------------
+    def get_emails(self):
+        """Return the email addresses associated with this contact."""
+        
+        emails = []
+        ai = GList.new(ebook.e_contact_get_attributes(hash(self._contact), E_CONTACT_EMAIL))
+        while ai.has_next():
+            attr = ai.next(as_a = EVCardAttribute)
+            if not attr:
+                raise Exception("Unexpected null attribute for [" + self._contact.get_name() + "] with emails " + emails)
+            emails.append(string_at(attr.value().next()))
+          
+        return emails
+        
+      
+      
+    # -----------------------------------------------------------------------
+    def get_urls(self):
+        """Return a list of URLs which are associated with this contact."""
+        
+        urls = []
+        ai = GList.new(ebook.e_contact_get_attributes(hash(self._contact), E_CONTACT_HOMEPAGE_URL))
+        while ai.has_next():
+            attr = ai.next(as_a = EVCardAttribute)
+            if not attr:
+                raise Exception("Unexpected null attribute for [" + self._contact.get_name() + "] with URLs " + urls)
+            urls.append(string_at(attr.value().next()))
+          
+        return urls
+    
+      
+    # -----------------------------------------------------------------------
+    def add_url(self, str, unique = ''):
+        """Add a new URL to the set of URLs for the given contact."""
+        
+        urls = re.findall('(?:(?:ftp|https?):\/\/|\\bwww\.|\\bftp\.)[,\w\.\-\/@:%?&=%+#~_$\*]+[\w=\/&=+#]', str, re.I | re.S)
+        updated = False
+        for url in urls:
+            updated = self._add_url(url, unique or re.sub('(?:.*://)?(\w+(?:[\w\.])*).*', '\\1', url)) or updated
+        
+        return updated
+    
+    
+    # -----------------------------------------------------------------------
+    def _add_url(self, url, unique):
+        """Do the work of adding a unique URL to a contact."""
+        
+        url_attr = None
+        ai = GList.new(ebook.e_contact_get_attributes(hash(self._contact), E_CONTACT_HOMEPAGE_URL))
+        while ai.has_next():
+            attr = ai.next(as_a = EVCardAttribute)
+            existing = string_at(attr.value().next())
+            #print "Existing URL [%s] when adding [%s] to [%s] with constraint [%s]" % (existing, url, contact.get_name(), unique)
+            if existing == unique or existing == url:
+                return False
+            elif existing.find(unique) > -1:
+                url_attr = attr
+          
+        if not url_attr:
+            ai.add()
+            url_attr = EVCardAttribute()
+            url_attr.group = ''
+            url_attr.name = 'URL'
+        
+        val = GList()
+        print "Setting URL for [%s] to [%s]" % (self._contact.get_name(), url)
+        val.set(create_string_buffer(url))
+        ai.set(addressof(url_attr))
+        url_attr.values = cast(addressof(val), POINTER(GList))
+        ebook.e_contact_set_attributes(hash(self._contact), E_CONTACT_HOMEPAGE_URL, addressof(ai))
+        return True
+
diff --git a/package/src/org/maemo/hermes/engine/facebook/__init__.py b/package/src/org/maemo/hermes/engine/facebook/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/package/src/org/maemo/hermes/engine/facebook/provider.py b/package/src/org/maemo/hermes/engine/facebook/provider.py
new file mode 100644 (file)
index 0000000..44661f3
--- /dev/null
@@ -0,0 +1,43 @@
+import org.maemo.hermes.engine.provider
+import org.maemo.hermes.engine.facebook.service
+
+class Provider(org.maemo.hermes.engine.provider.Provider):
+    """Facebook provider for Hermes. 
+
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+       Released under the Artistic Licence."""
+
+    # -----------------------------------------------------------------------
+    def get_name(self):
+        """Return the display name of this service. An icon, of with the lower-case,
+           all-alphabetic version of this name is expected to be provided."""
+           
+        return 'Facebook'
+    
+    
+    # -----------------------------------------------------------------------
+    def has_preferences(self):
+        """Whether or not this provider has any preferences. If it does not,
+           open_preferences must NOT be called; as the behaviour is undetermined."""
+           
+        return True
+    
+    
+    # -----------------------------------------------------------------------
+    def open_preferences(self, parent):
+        """Open the preferences for this provider as a child of the 'parent' widget."""
+
+        print "Err, open preferences. OK. Err, right. Hmm."
+
+    
+    # -----------------------------------------------------------------------
+    def service(self, gui_callback):
+        """Return the service backend. This must be a class which implements the
+           following methods:
+               * get_friends
+               * process_contact
+               * finalise
+        
+           See Service for more details."""
+           
+        return org.maemo.hermes.engine.facebook.service.Service()
diff --git a/package/src/org/maemo/hermes/engine/facebook/service.py b/package/src/org/maemo/hermes/engine/facebook/service.py
new file mode 100644 (file)
index 0000000..150b345
--- /dev/null
@@ -0,0 +1,156 @@
+import gnome.gconf
+import org.maemo.hermes.engine.service
+
+from facebook import Facebook
+from org.maemo.hermes.engine.names import canonical
+
+class Service(org.maemo.hermes.engine.service.Service):
+    """Facebook backend for Hermes.
+                
+       This requires two gconf paths to contain Facebook application keys:
+           /apps/maemo/hermes/key_app
+           /apps/maemo/hermes/key_secret
+       
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+       Released under the Artistic Licence."""
+       
+       
+    # -----------------------------------------------------------------------
+    def __init__(self, autocreate = False, gui_callback = None):
+        """Initialise the Facebook service, finding Facebook API keys in gconf and
+           having a gui_callback available."""
+        
+        self._gc  = gnome.gconf.client_get_default()
+        self._gui = gui_callback
+        self._autocreate = autocreate
+        
+        # -- Check the environment is going to work...
+        #
+        if (self._gc.get_string('/desktop/gnome/url-handlers/http/command') == 'epiphany %s'):
+            raise Exception('Browser in gconf invalid (see NB#136012). Installation error.')
+
+        key_app    = self._gc.get_string('/apps/maemo/hermes/key_app')
+        key_secret = self._gc.get_string('/apps/maemo/hermes/key_secret')
+        if key_app is None or key_secret is None:
+            raise Exception('No Facebook application keys found. Installation error.')
+
+        self.fb = Facebook(key_app, key_secret)
+        self.fb.desktop = True
+        
+        self._friends = None
+        self._friends_by_url = {}
+        self._should_create = set()
+
+
+    # -----------------------------------------------------------------------
+    def _do_fb_login(self):
+        """Perform authentication against Facebook and store the result in gconf
+             for later use. Uses the 'need_auth' and 'block_for_auth' methods on
+             the callback class. The former allows a message to warn the user
+             about what is about to happen to be shown; the second is to wait
+             for the user to confirm they have logged in."""
+        self.fb.session_key = None
+        self.fb.secret = None
+        self.fb.uid = None
+        
+        if self._gui:
+            self._gui.need_auth()
+            
+        self.fb.auth.createToken()
+        self.fb.login()
+        
+        if self._gui:
+            self._gui.block_for_auth()
+          
+        session = self.fb.auth.getSession()
+        self._gc.set_string('/apps/maemo/hermes/session_key', session['session_key'])
+        self._gc.set_string('/apps/maemo/hermes/secret_key', session['secret'])
+        self._gc.set_string('/apps/maemo/hermes/uid', str(session['uid']))
+
+   
+    # -----------------------------------------------------------------------
+    def get_friends(self):
+        """Return a list of friends from this service, or 'None' if manual mapping
+           is not supported."""
+           
+        if self._friends:
+            return self._friends.values()
+         
+        if self.fb.session_key is None:
+            self.fb.session_key = self._gc.get_string('/apps/maemo/hermes/session_key')
+            self.fb.secret = self._gc.get_string('/apps/maemo/hermes/secret_key')
+            self.fb.uid = self._gc.get_string('/apps/maemo/hermes/uid')
+        
+        # Check the available session is still valid...
+        while True:
+            try:
+                if self.fb.users.getLoggedInUser() and self.fb.session_key:
+                    break
+            except FacebookError:
+                pass
+            self._do_fb_login()
+            
+        self._friends = {}
+        attrs = ['uid', 'name', 'pic_big', 'birthday_date', 'profile_url', 'first_name', 'last_name', 'website']
+        for friend in self.fb.users.getInfo(self.fb.friends.get(), attrs):
+            print "#"
+            friend['key']= canonical(friend['name'])
+            self._friends[friend['key']] = friend
+        
+            if 'profile_url' not in friend:
+                friend['profile_url'] = "http://www.facebook.com/profile.php?id=" + str(friend ['uid'])
+        
+            self._friends_by_url[friend['profile_url']] = friend
+            friend['pic']  = friend[attrs[2]]
+            friend['account'] = 'Facebook'
+        
+            if 'website' in friend and friend['website']:
+                friend['homepage'] = friend['website']
+                
+            if 'birthday_date' in friend and friend['birthday_date']:
+                self._should_create.add(friend['profile_url'])
+                
+        return self._friends.values()
+
+    
+    # -----------------------------------------------------------------------
+    def process_contact(self, contact, overwrite = False, friend = None):
+        """Called for each contact in the address book. Any friends linked to
+           from the contact should have their matching updated. The backend should 
+           enrich the contact with any meta-data it can; and return 'True' if any
+           information was updated. If 'friend' is provided, the information should
+           be retrieved from friend. This will be one of the entries from 
+           'get_friends' when manual mapping is being done."""
+           
+        if not self._friends:
+            self.get_friends()
+            
+        matched_friend = None
+        for url in contact.get_urls():
+            if url in self._friends_by_url:
+                matched_friend = self._friends_by_url[url]
+                break
+
+        if not matched_friend:
+            for id in contact.get_identifiers():
+                if id in self._friends:
+                    matched_friend = self._friends[id]
+                    break
+                
+        if matched_friend:
+            print contact.get_name(), " -> ", matched_friend['profile_url']
+            self._should_create.discard(matched_friend['profile_url'])
+        else:
+            print contact.get_name(), " no match to Facebook with ", contact.get_identifiers()
+            
+    
+    
+    # -----------------------------------------------------------------------
+    def finalise(self, updated, overwrite = False):
+        """Once all contacts have been processed, allows for any tidy-up/additional
+           enrichment. If any contacts are updated at this stage, 'updated' should
+           be added to."""
+
+        if self._autocreate:
+            for url in self._should_create:
+                print "Need to autocreate:", self._friends_by_url[url]
diff --git a/package/src/org/maemo/hermes/engine/names.py b/package/src/org/maemo/hermes/engine/names.py
new file mode 100644 (file)
index 0000000..9df2ded
--- /dev/null
@@ -0,0 +1,72 @@
+import trans
+import re
+
+"""Utilities for determing name variants.
+
+   Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
+   Released under the Artistic Licence."""
+
+__non_alpha__ = re.compile("[^A-Za-z]+")
+
+__names__ = [
+    ['andrew', 'andy', 'andi', 'drew'],
+    ['benjamin', 'ben', 'benny'],
+    ['christian', 'chris'],
+    ['christopher', 'chris'],
+    ['david', 'dave'],
+    ['daniel', 'dan', 'danny'],
+    ['matthew', 'matt', 'mat', 'matty'],
+    ['melanie', 'mel'],
+    ['michael', 'mike', 'mic', 'mik', 'micky'],
+    ['peter', 'pete'],
+    ['robert', 'rob', 'bob', 'bobby', 'robbie'],
+    ['thomas', 'tom', 'tommy']
+  ]
+
+__map__ = {}
+for row in __names__:
+    for name in row:
+        if (not name in __map__):
+            __map__[name] = set(row)
+        else:
+            __map__[name] = __map__[name].union(row)
+
+
+# -----------------------------------------------------------------------
+def canonical(name, strip = True):
+    """Return a transliterated, lower-case version of name; optionally
+       stripping all non-alphabetic characters from the result."""
+    
+    try:
+        result = unicode(name).encode('trans').lower()
+        if strip:
+            return __non_alpha__.sub('', result)
+        else:
+            return result
+    except UnicodeDecodeError:
+        result = name.lower()
+        if strip:
+            return __non_alpha__.sub('', result)
+        else:
+            return result
+
+# -----------------------------------------------------------------------
+def variants(name):
+    """Return a set of names which should be checked for given the input
+       name. Any word which is has a replacement will be replaced, and an
+       iterable list of all variants will be returned."""
+
+    result = set()
+    if (name is None):
+        return result
+    
+    name = canonical(name, strip = False)
+    result.add(name)
+    bits = name.split(' ')
+    for bit in bits:
+        if (bit in __map__):
+            for replacement in __map__[bit]:
+                result.add(name.replace(bit, replacement))
+
+    return result
+
diff --git a/package/src/org/maemo/hermes/engine/provider.py b/package/src/org/maemo/hermes/engine/provider.py
new file mode 100644 (file)
index 0000000..01d2daf
--- /dev/null
@@ -0,0 +1,44 @@
+class Provider:
+    """The notional `Provider' which defines the interface which all service
+       providers for Hermes must meet. Implementations of this class are expected
+       to load their preferences on construction, display them through open_preferences
+       and pass them to the service object as necessary. 
+       
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+       Released under the Artistic Licence."""
+
+
+    # -----------------------------------------------------------------------
+    def get_name(self):
+        """Return the display name of this service. An icon, of with the lower-case,
+           all-alphabetic version of this name is expected to be provided."""
+           
+        return None
+    
+    
+    # -----------------------------------------------------------------------
+    def has_preferences(self):
+        """Whether or not this provider has any preferences. If it does not,
+           open_preferences must NOT be called; as the behaviour is undetermined."""
+           
+        return False
+    
+    
+    # -----------------------------------------------------------------------
+    def open_preferences(self, parent):
+        """Open the preferences for this provider as a child of the 'parent' widget."""
+        
+        pass
+
+    
+    # -----------------------------------------------------------------------
+    def service(self, gui_callback):
+        """Return the service backend. This must be a class which implements the
+           following methods:
+               * get_friends
+               * process_contact
+               * finalise
+        
+           See Service for more details."""
+           
+        return None
diff --git a/package/src/org/maemo/hermes/engine/service.py b/package/src/org/maemo/hermes/engine/service.py
new file mode 100644 (file)
index 0000000..eb5987b
--- /dev/null
@@ -0,0 +1,35 @@
+class Service:
+    """The notional `Service' for a provider. This is responsible for communicating
+       with the backend service and enhancing contacts.
+       
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+       Released under the Artistic Licence."""
+
+
+    # -----------------------------------------------------------------------
+    def get_friends(self):
+        """Return a list of friends from this service, or 'None' if manual mapping
+           is not supported."""
+         
+        return None
+
+    
+    # -----------------------------------------------------------------------
+    def process_contact(self, contact, overwrite = False, friend = None):
+        """Called for each contact in the address book. Any friends linked to
+           from the contact should have their matching updated. The backend should 
+           enrich the contact with any meta-data it can; and return 'True' if any
+           information was updated. If 'friend' is provided, the information should
+           be retrieved from friend. This will be one of the entries from 
+           'get_friends' when manual mapping is being done."""
+           
+        pass
+    
+    
+    # -----------------------------------------------------------------------
+    def finalise(self, updated, overwrite = False):
+        """Once all contacts have been processed, allows for any tidy-up/additional
+           enrichment. If any contacts are updated at this stage, 'updated' should
+           be added to."""
+           
+        pass
diff --git a/package/src/org/maemo/hermes/engine/twitter/__init__.py b/package/src/org/maemo/hermes/engine/twitter/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/package/src/org/maemo/hermes/engine/twitter/provider.py b/package/src/org/maemo/hermes/engine/twitter/provider.py
new file mode 100644 (file)
index 0000000..1e0b642
--- /dev/null
@@ -0,0 +1,53 @@
+import gnome.gconf
+import org.maemo.hermes.engine.provider
+import org.maemo.hermes.engine.twitter.service
+
+class Provider(org.maemo.hermes.engine.provider.Provider):
+    """Twitter provider for Hermes. 
+
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+       Released under the Artistic Licence."""
+
+
+    # -----------------------------------------------------------------------
+    def __init__(self):
+        self._gconf  = gnome.gconf.client_get_default()
+
+
+    # -----------------------------------------------------------------------
+    def get_name(self):
+        """Return the display name of this service. An icon, of with the lower-case,
+           all-alphabetic version of this name is expected to be provided."""
+           
+        return 'Twitter'
+    
+    
+    # -----------------------------------------------------------------------
+    def has_preferences(self):
+        """Whether or not this provider has any preferences. If it does not,
+           open_preferences must NOT be called; as the behaviour is undetermined."""
+           
+        return True
+    
+    
+    # -----------------------------------------------------------------------
+    def open_preferences(self, parent):
+        """Open the preferences for this provider as a child of the 'parent' widget."""
+
+        print "Err, open preferences. OK. Err, right. Hmm."
+
+    
+    # -----------------------------------------------------------------------
+    def service(self, gui_callback):
+        """Return the service backend. This must be a class which implements the
+           following methods:
+               * get_friends
+               * process_contact
+               * finalise
+        
+           See Service for more details."""
+    
+        username = self._gconf.get_string("/apps/maemo/hermes/twitter_user") or ''
+        password = self._gconf.get_string("/apps/maemo/hermes/twitter_pwd") or ''
+           
+        return org.maemo.hermes.engine.twitter.service.Service(username, password, gui_callback)
diff --git a/package/src/org/maemo/hermes/engine/twitter/service.py b/package/src/org/maemo/hermes/engine/twitter/service.py
new file mode 100644 (file)
index 0000000..f20384f
--- /dev/null
@@ -0,0 +1,89 @@
+import org.maemo.hermes.engine.service
+
+import twitter
+from org.maemo.hermes.engine.names import canonical
+
+class Service(org.maemo.hermes.engine.service.Service):
+    """Twitter backend for Hermes.
+       
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+       Released under the Artistic Licence."""
+       
+       
+    # -----------------------------------------------------------------------
+    def __init__(self, username, password, gui_callback = None):
+        """Initialise the Twitter service, using the given credentials and
+           having a gui_callback available."""
+        
+        self._gui = gui_callback
+        self._username = username
+        self._password = password
+        
+        self._friends = None
+        self._friends_by_url = {}
+
+   
+    # -----------------------------------------------------------------------
+    def get_friends(self):
+        """Return a list of friends from this service, or 'None' if manual mapping
+           is not supported."""
+           
+        if self._friends:
+            return self._friends.values()
+        
+        self._friends = {}
+        
+        api = twitter.Api(username=self._username, password=self._password)
+        for tweeter in api.GetFriends():
+            key    = canonical(tweeter.name)
+            url    = 'http://twitter.com/%s' % (tweeter.screen_name)
+            friend = {'name':          tweeter.name, 'pic': tweeter.profile_image_url,
+                      'birthday_date': None,         'profile_url': url,
+                      'homepage':      tweeter.url,  'account': 'twitter'}
+            if friend['pic'].find('/default_profile') > -1:
+                friend['pic'] = None
+          
+            self._friends[key] = friend
+            self._friends_by_url[url] = friend
+                
+        return self._friends.values()
+
+    
+    # -----------------------------------------------------------------------
+    def process_contact(self, contact, overwrite = False, friend = None):
+        """Called for each contact in the address book. Any friends linked to
+           from the contact should have their matching updated. The backend should 
+           enrich the contact with any meta-data it can; and return 'True' if any
+           information was updated. If 'friend' is provided, the information should
+           be retrieved from friend. This will be one of the entries from 
+           'get_friends' when manual mapping is being done."""
+           
+        if not self._friends:
+            self.get_friends()
+            
+        matched_friend = None
+        for url in contact.get_urls():
+            if url in self._friends_by_url:
+                matched_friend = self._friends_by_url[url]
+                break
+
+        if not matched_friend:
+            for id in contact.get_identifiers():
+                if id in self._friends:
+                    matched_friend = self._friends[id]
+                    break
+                
+        if matched_friend:
+            print contact.get_name(), " -> ", matched_friend['profile_url']
+        else:
+            print contact.get_name(), " no match to Twitter with ", contact.get_identifiers()
+            
+    
+    
+    # -----------------------------------------------------------------------
+    def finalise(self, updated, overwrite = False):
+        """Once all contacts have been processed, allows for any tidy-up/additional
+           enrichment. If any contacts are updated at this stage, 'updated' should
+           be added to."""
+           
+        pass
diff --git a/package/src/org/maemo/hermes/gui/__init__.py b/package/src/org/maemo/hermes/gui/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/package/src/org/maemo/hermes/gui/console.py b/package/src/org/maemo/hermes/gui/console.py
new file mode 100644 (file)
index 0000000..8b4f887
--- /dev/null
@@ -0,0 +1,20 @@
+class ConsoleUICallback:
+    """Meets the fb2contacts' authentication callback contract using
+       the console.
+    
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
+       Released under the Artistic Licence."""
+
+    # -----------------------------------------------------------------------
+    def need_auth(self):
+        print 'Need authentication...'
+
+    # -----------------------------------------------------------------------
+    def block_for_auth(self):
+        print 'Press enter when logged in...'
+        raw_input()
+    
+    # -----------------------------------------------------------------------
+    def progress(self, current, maximum):
+        print current, maximum
+
diff --git a/package/src/org/maemo/hermes/gui/contactview.py b/package/src/org/maemo/hermes/gui/contactview.py
new file mode 100644 (file)
index 0000000..569bb91
--- /dev/null
@@ -0,0 +1,72 @@
+import gtk
+import hildon
+import gobject
+import evolution
+from ctypes import *
+from pygobject import *
+
+class ContactView(hildon.PannableArea):
+    """Widget which shows a list of contacts in a pannable area.
+         
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
+       Released under the Artistic Licence."""
+    
+    
+    # -----------------------------------------------------------------------
+    def __init__(self, contacts):
+        """Constructor. Passed a list of EContacts."""
+        
+        hildon.PannableArea.__init__(self)
+        self.contacts = contacts
+        self.treestore = gtk.ListStore(str, gtk.gdk.Pixbuf, gobject.TYPE_PYOBJECT)
+        for contact in self.contacts:
+            if not contact.get_name():
+                continue
+              
+            photo = contact.get_property('photo')
+            pi = cast(c_void_p(hash(photo)), POINTER(EContactPhoto))
+            pixbuf = None
+            if pi.contents.data.uri.startswith("image/"):
+                data = string_at(pi.contents.data.inlined.data, pi.contents.data.inlined.length)
+                pixbuf_loader = gtk.gdk.PixbufLoader()
+                pixbuf_loader.write(data)
+                pixbuf_loader.close()
+                pixbuf = pixbuf_loader.get_pixbuf()
+            elif pi.contents.data.uri.startswith("file://"):
+                filename = pi.contents.data.uri[7:]
+                pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
+                  
+            if pixbuf:
+                size = min(pixbuf.get_width(), pixbuf.get_height())
+                pixbuf = pixbuf.subpixbuf(0, 0, size, size).scale_simple(48, 48, gtk.gdk.INTERP_BILINEAR)
+            self.treestore.append(row = [contact.get_name(), pixbuf, contact])
+        
+        self.treeview = gtk.TreeView(self.treestore)
+        tvcolumn = gtk.TreeViewColumn('Name', gtk.CellRendererText(), text = 0)
+        self.treeview.append_column(tvcolumn)
+        
+        cell = gtk.CellRendererPixbuf()
+        cell.set_property('xalign', 1.0)
+        tvcolumn = gtk.TreeViewColumn('Picture', cell, pixbuf = 1)
+        self.treeview.append_column(tvcolumn)
+        
+        self.treeview.set_search_column(0)
+        self.treeview.connect('row-activated', self._activated)
+        self.add(self.treeview)
+        self.set_size_request(600, 380)
+      
+      
+    # -----------------------------------------------------------------------
+    def _activated(self, treeview, path, column):
+        """Used to emit the `contact-activated' signal once a row has been
+           selected."""
+        
+        iter    = treeview.get_model().get_iter(path)
+        contact = treeview.get_model().get_value(iter, 2)
+        self.emit('contact-activated', contact)
+
+
+_contact_activated = gobject.signal_new('contact-activated', ContactView, gobject.SIGNAL_ACTION, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT])
+
+
+
diff --git a/package/src/org/maemo/hermes/gui/gtk.py b/package/src/org/maemo/hermes/gui/gtk.py
new file mode 100644 (file)
index 0000000..9259993
--- /dev/null
@@ -0,0 +1,283 @@
+import gettext
+import gtk, gobject
+import traceback
+import time
+import thread
+from org.maemo.hermes.gui.contactview import ContactView
+import urllib2
+import hildon
+from org.bleb.wimpworks import WimpWorks
+from org.maemo.hermes.gui.mapcontact import MapContact
+from hermes import Hermes
+
+class HermesGUI(WimpWorks):
+    """Provides the GUI for Hermes, allowing the syncing of Facebook and
+       Twitter friends' information with the Evolution contacts' database.
+       
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
+       Released under the Artistic Licence."""
+
+
+    # -----------------------------------------------------------------------
+    def __init__(self):
+        gettext.install('hermes','/opt/hermes/share/locale/')
+        WimpWorks.__init__(self, 'Hermes', version = '0.2.0', dbus_name = 'org.maemo.hermes')
+        self.set_background('background.png')
+        
+        layout = org.bleb.wimpworks.HildonMainScreenLayout(offset = 0.8, container = self)
+        layout.add_button('Retrieve', _("Get contacts' missing info"))
+        layout.add_button('Refresh', _("Update contacts' info"))
+        
+        self.add_menu_action("Accounts")
+        self.menu.show_all()
+
+  
+    # -----------------------------------------------------------------------
+    def do_retrieve(self, widget):
+        self.sync(widget, False)
+    
+    
+    # -----------------------------------------------------------------------
+    def do_refresh(self, widget):
+        self.sync(widget, True)
+
+
+    # -----------------------------------------------------------------------
+    def do_accounts(self, widget = None):
+        dialog = gtk.Dialog(_('Accounts'), self.main_window)
+        dialog.add_button(_('Save'), gtk.RESPONSE_OK)
+        
+        #pa = hildon.PannableArea()
+        #dialog.vbox.add(pa)
+        content = dialog.vbox 
+        #content = gtk.VBox()
+        #pa.add(content)
+        #pa.set_size_request(600, 380)
+        
+        use_facebook = self.new_checkbox(_('Use Facebook'), content)
+        use_facebook.set_active(self.get_use_facebook())
+        
+        indent = self.new_indent(content)
+        self.link_control(use_facebook, gtk.Label(_('Note: authentication via web page')), indent)
+        
+        fb_empty = self.link_control(use_facebook, self.new_checkbox(_('Create birthday-only contacts')), indent)
+        fb_empty.set_active(self.get_create_empty())
+        
+        use_twitter = self.new_checkbox(_('Use Twitter'), content)
+        use_twitter.set_active(self.get_use_twitter())
+        
+        indent = self.new_indent(content)
+        tw_user = self.link_control(use_twitter, self.new_input(_('Twitter username')), indent)
+        tw_user.set_text(self.get_twitter_credentials()[0])
+        
+        tw_pass = self.link_control(use_twitter, self.new_input(_('Twitter password'), password = True), indent)
+        tw_pass.set_text(self.get_twitter_credentials()[1])
+        
+        dialog.show_all()
+        result = dialog.run()
+        dialog.hide()
+        if result == gtk.RESPONSE_OK:
+            self.set_use_facebook(use_facebook.get_active())
+            self.set_create_empty(fb_empty.get_active())
+            self.set_use_twitter(use_twitter.get_active(), tw_user.get_text(), tw_pass.get_text())
+        
+        return result
+   
+
+    # -----------------------------------------------------------------------
+    def sync(self, widget, force, main = True):
+        if main and not self.get_use_facebook() and not self.get_use_twitter():
+            saved = self.do_accounts()
+            if saved == gtk.RESPONSE_DELETE_EVENT:
+                return
+        
+        if main:
+            self.main_window.set_property('sensitive', False)
+            thread.start_new_thread(self.sync, (widget, force, False))
+        else:
+            try:
+                fb2c = Hermes(self,
+                              twitter = (self.get_use_twitter() and self.get_twitter_credentials()) or None,
+                              facebook = self.get_use_facebook(),
+                              empty = self.get_create_empty())
+                fb2c.load_friends()
+                fb2c.sync_contacts(resync = force)
+                gobject.idle_add(self.open_summary, fb2c)
+        
+            except urllib2.HTTPError, e:
+                traceback.print_exc()
+                if e.code == 401:
+                    gobject.idle_add(self.report_error, _('Authentication problem. Check credentials.'), True)
+                else:
+                    gobject.idle_add(self.report_error, _('Network connection error. Check connectivity.'))
+        
+            except urllib2.URLError, e:
+                traceback.print_exc()
+                gobject.idle_add(self.report_error, _('Network connection error. Check connectivity.'))
+          
+            except Exception, e:
+                traceback.print_exc()
+                gobject.idle_add(self.report_error, _('Something went wrong: ') + e.message)
+    
+    
+    # -----------------------------------------------------------------------
+    def open_summary(self, fb2c):
+        gobject.idle_add(self.main_window.set_property, 'sensitive', True)
+    
+        dialog = gtk.Dialog(_('Summary'), self.main_window)
+        dialog.add_button(_('Done'), gtk.RESPONSE_OK)
+      
+        button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
+                               title = _('Updated %d contacts') % (len(fb2c.updated)))
+        button.connect('clicked', self.show_contacts, fb2c, fb2c.updated)
+        button.set_property('sensitive', len(fb2c.updated) > 0)
+        dialog.vbox.add(button)
+        
+        button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
+                               title = _('Matched %d contacts') % (len(fb2c.matched)))
+        button.connect('clicked', self.show_contacts, fb2c, fb2c.matched)
+        button.set_property('sensitive', len(fb2c.matched) > 0)
+        dialog.vbox.add(button)
+      
+        button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
+                               title = _('%d contacts unmatched') % (len(fb2c.unmatched)))
+        button.connect('clicked', self.show_contacts, fb2c, fb2c.unmatched)
+        button.set_property('sensitive', len(fb2c.unmatched) > 0)
+        dialog.vbox.add(button)
+      
+        dialog.show_all()
+        dialog.run()
+        dialog.hide()
+    
+    
+    # -----------------------------------------------------------------------
+    def show_contacts(self, widget, fb2c, contacts):
+        view = ContactView(contacts)
+    
+        dialog = gtk.Dialog(_('Contacts'), self.main_window)
+        view.connect('contact-activated', self.map_contact, fb2c)
+        dialog.vbox.add(view)
+        dialog.show_all()
+      
+        dialog.run()
+        dialog.hide()
+      
+      
+    # -----------------------------------------------------------------------
+    def map_contact(self, widget, contact, fb2c):
+        view = MapContact(fb2c.friends, contact)
+    
+        dialog = gtk.Dialog(contact.get_name(), self.main_window)
+        dialog.add_button(_('Update'), gtk.RESPONSE_OK)
+        dialog.vbox.add(view)
+        dialog.show_all()
+      
+        result = dialog.run()
+        dialog.hide()
+        if result == gtk.RESPONSE_OK:
+            friend = view.get_selected_friend()
+            if friend:
+                if 'contact' in friend and friend['contact'] == contact:
+                    hildon.hildon_banner_show_information(self.main_window, '', _("Removing existing mappings is not yet supported"))
+                elif view.contact_mapped:
+                    if fb2c.update_contact(contact, friend, True):
+                        fb2c.addresses.commit_contact(contact)
+                else:
+                    if fb2c.update_contact(contact, friend, False):
+                        fb2c.addresses.commit_contact(contact)
+    
+      
+    # -----------------------------------------------------------------------
+    def need_auth(self, main = False):
+        if main:
+            hildon.hildon_banner_show_information(self.main_window, '', _("Need to authenticate with Facebook"))
+        else:
+            gobject.idle_add(self.need_auth, True)
+      
+    
+    # -----------------------------------------------------------------------
+    def block_for_auth(self, main = False, lock = None):
+        if main:
+            note = gtk.Dialog(_('Facebook authorisation'), self.main_window)
+            note.add_button(_("Validate"), gtk.RESPONSE_OK)
+            note.vbox.add(gtk.Label(_("\nPress 'Validate' once Facebook has\nbeen authenticated in web browser.\n")))
+    
+            note.show_all()
+            result = note.run()
+            note.hide()
+            lock.release()
+        
+        else:
+            time.sleep(2)
+            lock = thread.allocate_lock()
+            lock.acquire()
+            gobject.idle_add(self.block_for_auth, True, lock)
+            lock.acquire()
+            lock.release()
+    
+    
+    # -----------------------------------------------------------------------
+    def progress(self, i, j, main = False):
+        if main:
+            if i == 0:
+                self.progressbar = gtk.ProgressBar()
+                self.progressnote = gtk.Dialog(_("Fetching friends' info"), self.main_window)
+                self.progressnote.vbox.add(self.progressbar)
+                hildon.hildon_gtk_window_set_progress_indicator(self.progressnote, 1)
+               
+                self.progressnote.show_all()
+              
+            elif i < j:
+                if i == 1:
+                    self.progressnote.set_title(_("Updating contacts"))
+                    hildon.hildon_gtk_window_set_progress_indicator(self.progressnote, 0)
+                
+                self.progressbar.set_fraction(float(i) / float(j))
+              
+            else:
+                self.progressnote.destroy()
+              
+            print i,j
+        else:
+            gobject.idle_add(self.progress, i, j, True)
+    
+    
+    # -----------------------------------------------------------------------
+    def report_error(self, e, prefs = False):
+        if self.progressnote:
+            self.main_window.set_property('sensitive', True)
+            self.progressnote.destroy()
+    
+        hildon.hildon_banner_show_information(self.main_window, '', e)
+        if prefs:
+            self.do_accounts()
+    
+        
+    def get_use_facebook(self):
+        return self.gconf.get_bool("/apps/maemo/hermes/use_facebook")
+    
+    
+    def set_use_facebook(self, value):
+        self.gconf.set_bool("/apps/maemo/hermes/use_facebook", value)
+    
+    def get_create_empty(self):
+        return self.gconf.get_bool("/apps/maemo/hermes/create_empty")
+    
+    
+    def set_create_empty(self, value):
+        self.gconf.set_bool("/apps/maemo/hermes/create_empty", value)
+    
+    
+    def get_use_twitter(self):
+        return self.gconf.get_bool("/apps/maemo/hermes/use_twitter")
+    
+    
+    def set_use_twitter(self, value, user, password):
+        self.gconf.set_bool("/apps/maemo/hermes/use_twitter", value)
+        self.gconf.set_string("/apps/maemo/hermes/twitter_user", user)
+        self.gconf.set_string("/apps/maemo/hermes/twitter_pwd", password)
+      
+    
+    def get_twitter_credentials(self):
+        return (self.gconf.get_string("/apps/maemo/hermes/twitter_user") or '',
+                self.gconf.get_string("/apps/maemo/hermes/twitter_pwd") or '')
diff --git a/package/src/org/maemo/hermes/gui/mapcontact.py b/package/src/org/maemo/hermes/gui/mapcontact.py
new file mode 100644 (file)
index 0000000..ca668cd
--- /dev/null
@@ -0,0 +1,77 @@
+import gtk
+import hildon
+from ctypes import *
+from pygobject import *
+
+class MapContact(hildon.PannableArea):
+    """Widget which shows a list of friends from various feeds and allows
+       the mapping to a particular contact.
+       
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
+       Released under the Artistic Licence."""
+
+
+    # -----------------------------------------------------------------------
+    def __init__(self, friends, contact):
+        """Constructor. Passed a list of `friends' and the contact we're mapping."""
+        
+        hildon.PannableArea.__init__(self)
+        self.friends = friends
+        self.contact = contact
+        self.treestore = gtk.ListStore(gtk.gdk.Pixbuf, str, gtk.gdk.Pixbuf, gobject.TYPE_PYOBJECT)
+        
+        accounts = {}
+        _facebook = gtk.gdk.pixbuf_new_from_file('/opt/hermes/share/account-facebook.png')
+        _twitter  = gtk.gdk.pixbuf_new_from_file('/opt/hermes/share/account-twitter.png')
+        _tick     = gtk.icon_theme_get_default().load_icon('widgets_tickmark_list', 48, 0)
+        
+        self.contact_mapped = False
+        mapped_iter = None
+        for key in sorted(self.friends.keys(), cmp = lambda a, b: cmp(a.lower(), b.lower())):
+            friend = self.friends[key]
+            if friend['account'] not in accounts:
+                accounts[friend['account']] = gtk.gdk.pixbuf_new_from_file("/opt/hermes/share/account-%s.png" % (friend['account']))
+              
+            photo = friend['pic']
+            pixbuf = None
+            if 'contact' in friend:
+                if friend['contact'] == contact:
+                    pixbuf = _tick
+                    self.contact_mapped = True
+                    mapped_iter = self.treestore.append([accounts[friend['account']], friend['name'], pixbuf, friend])
+                else:
+                    continue
+            else:
+                self.treestore.append([accounts[friend['account']], friend['name'], pixbuf, friend])
+        
+        self.treeview = gtk.TreeView(self.treestore)
+        hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
+        
+        self.treeview.append_column(gtk.TreeViewColumn('Account', gtk.CellRendererPixbuf(), pixbuf = 0))
+        self.treeview.append_column(gtk.TreeViewColumn('Name', gtk.CellRendererText(), text = 1))
+        
+        cell = gtk.CellRendererPixbuf()
+        cell.set_property('xalign', 1.0)
+        self.treeview.append_column(gtk.TreeViewColumn('Picture', cell, pixbuf = 2))
+        
+        if mapped_iter:
+            path = self.treestore.get_path(mapped_iter)
+            self.treeview.get_selection().select_path(path)
+            self.treeview.scroll_to_cell(path)
+          
+        self.add(self.treeview)
+        self.set_size_request(600, 320)
+      
+    # -----------------------------------------------------------------------
+    def get_selected_friend(self):
+        """Return the selected friend, or `None' if none."""
+    
+        (model, iter) = self.treeview.get_selection().get_selected()
+        if not iter:
+            return None
+             
+        return model.get_value(iter, 3)
+
+
+_account_selected = gobject.signal_new('account-selected', MapContact, gobject.SIGNAL_ACTION, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])
+
diff --git a/package/src/test.py b/package/src/test.py
new file mode 100644 (file)
index 0000000..46fbd6e
--- /dev/null
@@ -0,0 +1,27 @@
+#!/usr/bin/python2.5
+
+import evolution
+from org.maemo.hermes.engine.contact import Contact
+from org.maemo.hermes.gui.console import ConsoleUICallback
+import org.maemo.hermes.engine.facebook.provider
+import org.maemo.hermes.engine.twitter.provider
+
+
+providers = [
+     org.maemo.hermes.engine.facebook.provider.Provider(),
+     org.maemo.hermes.engine.twitter.provider.Provider()
+]
+             
+ui = ConsoleUICallback()
+services = []
+for provider in providers:
+    print "Using %s" % (provider.get_name())
+    services.append(provider.service(ui))
+
+addresses = evolution.ebook.open_addressbook('default')
+for econtact in addresses.get_all_contacts():
+    contact = Contact(addresses, econtact)
+    print "+++ %s" % (contact.get_name())
+    for service in services:
+        print service.process_contact(contact)
+
diff --git a/package/src/wimpworks.py b/package/src/wimpworks.py
deleted file mode 100644 (file)
index 4f602f2..0000000
+++ /dev/null
@@ -1,284 +0,0 @@
-#
-# WimpWorks (for Python)                (c) Andrew Flegg 2009.
-# ~~~~~~~~~~~~~~~~~~~~~~                Released under the Artistic Licence.
-#                                       http://www.bleb.org/
-
-import gettext
-import gtk, gobject
-import re
-import thread
-import os.path
-
-# -- Work out environment...
-#
-try:
-    import hildon
-    _have_hildon = True
-except ImportError:
-    _have_hildon = False
-  
-try:
-    import osso
-    _have_osso = True
-except ImportError:
-    _have_osso = False
-  
-try:
-    import gnome.gconf
-    _have_gconf = True
-except ImportError:
-    _have_gconf = False
-
-gobject.threads_init()
-
-# -- Main class...
-#
-class WimpWorks:
-    '''A framework for creating easy-to-use graphical user interfaces using
-       GTK+, Python, DBus and more.
-       
-       This is the base class. It should be constructed with a DBus name
-       and a version.
-         
-       Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
-       Released under the Artistic Licence.'''
-    
-    
-    # -----------------------------------------------------------------------
-    def __init__(self, application, version = '1.0.0', dbus_name = None):
-        '''Constructor. Initialises the gconf connection, DBus, OSSO and more.
-        
-           @param application User-facing name of the application.
-           @param version Version string of the application.
-           @param dbus_name Name to register with DBus. If unspecified, no
-                  DBus registration will be performed.'''
-        
-        self.name = application
-        self.dbus_name = dbus_name
-        self.menu = None
-        
-        if _have_gconf:
-            self.gconf = gnome.gconf.client_get_default()
-        
-        if _have_hildon:
-            self.app = hildon.Program()
-            self.main_window = hildon.Window()
-            gtk.set_application_name(application)
-        else:
-            self.app = None
-            self.main_window = gtk.Window()
-        
-        self.main_window.set_title(application)
-        self.main_window.connect("delete-event", gtk.main_quit)
-        
-        if _have_osso and dbus_name:
-            self.osso_context = osso.Context(dbus_name, version, False)
-          
-        if self.app:
-            self.app.add_window(self.main_window)
-          
-        if _have_hildon:
-            self._expose_hid = self.main_window.connect('expose-event', self._take_screenshot)
-    
-        
-    # -----------------------------------------------------------------------
-    def set_background(self, file, window = None):
-        '''Set the background of the given (or main) window to that contained in
-           'file'.
-           
-           @param file File name to set. If not an absolute path, typical application
-                       directories will be checked.
-           @param window Window to set background of. If unset, will default to the
-                         main application window.'''
-        
-        # TODO Handle other forms of path
-        file = "/opt/%s/share/%s" % (re.sub('[^a-z0-9_]', '', self.name.lower()), file)
-        if not window:
-            window = self.main_window
-          
-        self._background, mask = gtk.gdk.pixbuf_new_from_file(file).render_pixmap_and_mask()
-        window.realize()
-        window.window.set_back_pixmap(self._background, False)
-    
-      
-    # -----------------------------------------------------------------------
-    def add_menu_action(self, title, window = None):
-        '''Add a menu action to the given (or main) window. Once add_menu_action()
-           has been called with all the properties, 'self.menu.show_all()' should be
-           called.
-        
-           @param title The label of the action, and used to compute the callback
-                        method. This should be the UN-i18n version: gettext is used
-                        on the value.'''
-        
-        if not window:
-            window = self.main_window
-          
-        if not self.menu:
-            if _have_hildon:
-                self.menu = hildon.AppMenu()
-                window.set_app_menu(self.menu)
-            else:
-                raise Exception("Menu needs to be created, and no Hildon present")
-            
-        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-        button.set_label(_(title))
-        button.connect("clicked", self.callback, title)
-        self.menu.append(button)
-    
-    
-    # -----------------------------------------------------------------------
-    def run(self):
-        '''Once the application has been initialised, this will show the main window
-           and run the mainloop.'''
-         
-        self.main_window.show_all()
-        gtk.main()
-    
-    
-    # -----------------------------------------------------------------------
-    def _take_screenshot(self, event = None, data = None):
-        '''Used to provide a quick-loading screen.
-        
-           @see http://maemo.org/api_refs/5.0/5.0-final/hildon/hildon-Additions-to-GTK+.html#hildon-gtk-window-take-screenshot'''
-        
-        self.main_window.disconnect(self._expose_hid)
-        if not os.path.isfile("/home/user/.cache/launch/%s.pvr" % (self.dbus_name)):
-            gobject.timeout_add(80, hildon.hildon_gtk_window_take_screenshot, self.main_window, True)
-    
-      
-    # -----------------------------------------------------------------------
-    def callback(self, event, method):
-        '''Call a method on this object, using the given string to derive
-           the name. If no method is found, no action is taken.
-           
-           @param event Event which triggered the callback.
-           @param method String which will be lowercased to form a method
-                  called 'do_method'.'''
-        
-        method = re.sub('[^a-z0-9_]', '', method.lower())
-        getattr(self, "do_%s" % (method))(event.window)
-    
-      
-    # -----------------------------------------------------------------------
-    def new_checkbox(self, label, box = None):
-        '''Create a new checkbox, adding it to the given container.
-        
-           @param label Label for the checkbox.
-           @param box Optional container to add the created checkbox to.
-           @return The newly created checkbox.'''
-           
-        checkbox = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
-        checkbox.set_label(label)
-        if box:
-            box.add(checkbox)
-        return checkbox
-    
-    
-    # -----------------------------------------------------------------------
-    def new_indent(self, box):
-        '''Create an indent which can be used to show items related to each other.
-        
-           @param box Container to add the indent to.'''
-           
-        outer = gtk.HBox()
-        indent = gtk.VBox()
-        outer.pack_start(indent, padding=48)
-        box.add(outer)
-        return indent
-    
-    
-    # -----------------------------------------------------------------------
-    def new_input(self, label, box = None, password = False):
-        '''Create a new input with the given label, optionally adding it to a
-           container.
-           
-           @param label Text describing the purpose of the input field.
-           @param box Optional container to add the input to.
-           @param password Boolean indicating if the input is used for passwords.
-           @return The newly created input.'''
-           
-        input = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
-        input.set_placeholder(label)
-        input.set_property('is-focus', False)
-        
-        if password:
-            input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL | gtk.HILDON_GTK_INPUT_MODE_INVISIBLE)
-        else:
-            input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL)
-          
-        if box:
-            box.add(input)
-        return input
-    
-    
-    # -----------------------------------------------------------------------
-    def link_control(self, checkbox, ctrl, box = None):
-        '''Link a checkbox to a control, such that the editability of the
-           control is determined by the checkbox state.
-           
-           @param checkbox Checkbox which will control the state.
-           @param ctrl Control to add.
-           @param box Optional container to add 'ctrl' to.
-           @return The added control.'''
-           
-        if box:
-            box.add(ctrl)
-          
-        self._sync_edit(checkbox, ctrl)
-        checkbox.connect('toggled', self._sync_edit, ctrl)
-        return ctrl
-    
-      
-    # -----------------------------------------------------------------------
-    def _sync_edit(self, checkbox, edit):
-        edit.set_property('sensitive', checkbox.get_active())
-    
-
-      
-# -----------------------------------------------------------------------
-class HildonMainScreenLayout():
-    '''Provides a mechanism for creating a traditional multi-button button
-       selection, as made popular by Maemo 5's Media Player, Clock, Application
-       Manager and HIG.
-       
-       This does *not* require Hildon, however.
-    '''
-
-    # ---------------------------------------------------------------------
-    def __init__(self, container, offset = 0.5):
-        '''Create a new layout.
-        
-           @param container Container to add layout to. If unspecified,
-                  the application's main window will be used.
-           @param offset The vertical offset for the buttons. If unspecified,
-                  they will be centred. Ranges from 0.0 (top) to 1.0 (bottom).'''
-                  
-        self._container = container
-        alignment = gtk.Alignment(xalign=0.5, yalign=0.8, xscale=0.8)
-        self._box = gtk.HButtonBox()
-        alignment.add(self._box)
-        container.main_window.add(alignment)
-        self._box.set_property('layout-style', gtk.BUTTONBOX_SPREAD)
-
-      
-    # ---------------------------------------------------------------------
-    def add_button(self, title, subtitle = ''):
-        '''Add a button to the layout with the specified title. Upon clicking
-           the button, a method of the name 'do_title' will be invoked on the
-           main class.
-           
-           @param title Value of the button, and used to derive the callback method. This
-                        should be the UN-i18n version: gettext is used on the value.
-           @param subtitle An optional subtitle containing more information.'''
-        
-        if _have_hildon:         
-            button = hildon.Button(gtk.HILDON_SIZE_THUMB_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
-                                 title = title, value = subtitle)
-        else:
-            button = gtk.Button(label = _(title))
-        
-        button.set_property('width-request', 250)
-        button.connect('clicked', self._container.callback, title)
-        self._box.add(button)
-