+++ /dev/null
-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
-
+++ /dev/null
-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
-
+++ /dev/null
-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])
-
-
-
+++ /dev/null
-#!/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()
-
--- /dev/null
+#!/usr/bin/env python
+
+from org.maemo.hermes.gui import HermesGUI
+gui = HermesGUI()
+gui.run()
+
+++ /dev/null
-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])
-
+++ /dev/null
-"""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
-
--- /dev/null
+#
+# 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)
+
--- /dev/null
+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
--- /dev/null
+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
+
--- /dev/null
+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()
--- /dev/null
+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]
--- /dev/null
+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
+
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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)
--- /dev/null
+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
--- /dev/null
+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
+
--- /dev/null
+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])
+
+
+
--- /dev/null
+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 '')
--- /dev/null
+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])
+
--- /dev/null
+#!/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)
+
+++ /dev/null
-#
-# 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)
-