--- /dev/null
+compile:
+ py_compilefiles src/*.py
+ mkdir bin
+ gcc -o bin/contact-update -std=c99 `pkg-config --cflags --libs libebook-1.2 glib-2.0` src/contact-update.c
+
+install:
+ mkdir -p ${DESTDIR}/opt/hermes/lib ${DESTDIR}/opt/hermes/bin
+ ln -s ../lib/gui.py ${DESTDIR}/opt/hermes/bin/hermes
+ install -D -m 0755 -o root -g root bin/contact-update ${DESTDIR}/opt/hermes/bin/contact-update
+ install -D -m 0644 -o root -g root src/*.py* ${DESTDIR}/opt/hermes/lib/
+ install -D -m 0644 -o root -g root share/hermes-64.png ${DESTDIR}/usr/share/icons/hicolor/scalable/hermes.png
+ install -D -m 0644 -o root -g root share/hermes-48.png ${DESTDIR}/usr/share/icons/hicolor/48x48/hermes.png
+ install -D -m 0644 -o root -g root share/hermes.desktop ${DESTDIR}/usr/share/applications/hildon/hermes.desktop
+ install -D -m 0644 -o root -g root share/hermes.service ${DESTDIR}/usr/share/dbus-1/services/hermes.service
+ chmod 755 ${DESTDIR}/opt/hermes/lib/gui.py
+
+clean:
+ rm -f src/*.py[oc]
+ rm -f build-stamp configure-stamp
+ rm -rf debian/hermes bin
+ find . -name *~ -exec rm -f {} \;
--- /dev/null
+The Debian Package hermes
+----------------------------
+
+Comments regarding the Package
+
+ -- unknown <andrew@bleb.org> Wed, 30 Sep 2009 21:21:27 +0100
--- /dev/null
+hermes for Debian
+-----------------
+
+<possible notes regarding this package - if none, delete this file>
+
+ -- unknown <andrew@bleb.org> Wed, 30 Sep 2009 21:21:27 +0100
--- /dev/null
+hermes (0.0.1) unstable; urgency=low
+
+ * Initial Release.
+
+ -- unknown <andrew@bleb.org> Wed, 30 Sep 2009 21:21:27 +0100
--- /dev/null
+Source: hermes
+Section: user/utilities
+Priority: extra
+Maintainer: Andrew Flegg <andrew@bleb.org>
+Build-Depends: debhelper (>= 5), python-runtime | python2.5-runtime, libebook-dev
+Standards-Version: 3.7.2
+
+Package: hermes
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}, python-imaging |
+ python2.5-imaging, python-osso | python2.5-osso, python-hildon |
+ python2.5-hildon, python-twitter, python-facebook, python-evolution
+Description: Enrich contacts' information from social networks.
+ Hermes, the Greek god of communication, will fill in the gaps in your
+ contacts' address book. Photos and birthdays for your friends on
+ social networks like Facebook and Twitter will be used to complete
+ the jigsaw.
+XB-Maemo-Display-Name: Hermes
+XB-Maemo-Icon-26:
+ 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
+ YWdlUmVhZHlxyWU8AAAAAElFTkSuQmCC
+
--- /dev/null
+This is hermes, written and maintained by unknown <andrew@bleb.org>
+on Wed, 30 Sep 2009 21:21:27 +0100.
+
+The original source can always be found at:
+ ftp://ftp.debian.org/dists/unstable/main/source/
+
+Copyright Holder: unknown
+
+License:
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this package; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+On Debian systems, the complete text of the GNU General
+Public License can be found in `/usr/share/common-licenses/GPL'.
--- /dev/null
+usr/bin
+usr/sbin
--- /dev/null
+#!/bin/sh
+
+set -e
+gconftool-2 -s /apps/maemo/hermes/key_app 5916f12942feea4b3247d42a84371112 --type string
+gconftool-2 -s /apps/maemo/hermes/key_secret 19f7538edd96b6870f2da7e84a6390a4 --type string
+
+# Hacky fix for NB#136012
+gconftool-2 -s /desktop/gnome/url-handlers/http/command 'dbus-send --system --type=method_call --dest="com.nokia.osso_browser" --print-reply /com/nokia/osso_browser/request com.nokia.osso_browser.load_url string:"%s"' --type string
+
+gtk-update-icon-cache -f /usr/share/icons/hicolor
--- /dev/null
+#!/usr/bin/make -f
+# -*- makefile -*-
+# Sample debian/rules that uses debhelper.
+# This file was originally written by Joey Hess and Craig Small.
+# As a special exception, when this file is copied by dh-make into a
+# dh-make output file, you may use that output file without restriction.
+# This special exception was added by Craig Small in version 0.37 of dh-make.
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+
+
+
+CFLAGS = -Wall -g
+
+ifneq (,$(findstring noopt,$(DEB_BUILD_OPTIONS)))
+ CFLAGS += -O0
+else
+ CFLAGS += -O2
+endif
+
+configure: configure-stamp
+configure-stamp:
+ dh_testdir
+ # Add here commands to configure the package.
+
+ touch configure-stamp
+
+
+build: build-stamp
+
+build-stamp: configure-stamp
+ dh_testdir
+
+ # Add here commands to compile the package.
+ $(MAKE)
+ #docbook-to-man debian/hermes.sgml > hermes.1
+
+ touch $@
+
+clean:
+ dh_testdir
+ dh_testroot
+ rm -f build-stamp configure-stamp
+
+ # Add here commands to clean up after the build process.
+ -$(MAKE) clean
+
+ dh_clean
+
+install: build
+ dh_testdir
+ dh_testroot
+ dh_clean -k
+ dh_installdirs
+
+ # Add here commands to install the package into debian/hermes.
+ $(MAKE) DESTDIR=$(CURDIR)/debian/hermes install
+
+
+# Build architecture-independent files here.
+binary-indep: build install
+# We have nothing to do by default.
+
+# Build architecture-dependent files here.
+binary-arch: build install
+ dh_testdir
+ dh_testroot
+ dh_installchangelogs
+# dh_installdocs
+ dh_installexamples
+# dh_install
+# dh_installmenu
+# dh_installdebconf
+# dh_installlogrotate
+# dh_installemacsen
+# dh_installpam
+# dh_installmime
+# dh_python
+# dh_installinit
+# dh_installcron
+# dh_installinfo
+# dh_installman
+ dh_link
+ dh_strip
+ dh_compress
+ dh_fixperms
+# dh_perl
+# dh_makeshlibs
+ dh_installdeb
+ dh_shlibdeps
+ dh_gencontrol
+ dh_md5sums
+ dh_builddeb
+
+binary: binary-indep binary-arch
+.PHONY: build clean binary-indep binary-arch binary install configure
--- /dev/null
+[Desktop Entry]
+Version=1.0
+Encoding=UTF-8
+Name=Hermes
+Icon=hermes
+Exec=/opt/hermes/bin/hermes
+X-Osso-Service=org.maemo.hermes
+Type=Application
--- /dev/null
+[D-BUS Service]
+Name=org.maemo.hermes
+Exec=/opt/hermes/bin/hermes
--- /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
+/**
+ * contact-update (c) Andrew Flegg 2009
+ * ~~~~~~~~~~~~~~ Released under the Artistic Licence.
+ *
+ * A very quick and dirty program to modify an EDS contact's photo and
+ * birthday. This is necessary because evolution-python only exposes the two
+ * structs GBoxed, which cannot be created or manipulated in Python (AFAICT).
+ *
+ * Error handling: limited
+ * Syntax: contact-update <name> --photo <mime-type> <file>
+ * contact-update <name> --birthday <year> <month> <day>
+ */
+#include <libebook/e-book.h>
+#include <libebook/e-contact.h>
+#include <glib.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <string.h>
+
+// gcc -o contact-update -std=c99 `pkg-config --cflags --libs libebook-1.2 glib-2.0` contact-update.c
+
+GError *error = NULL;
+
+
+/**
+ * Simple error handling.
+ *
+ * @param result Result of an expression. If false, <var>message</var> will be shown
+ * and the process terminated.
+ * @param message Message to display if <var>result</var> is false.
+ */
+void* try(gboolean result, char* message) {
+ if (!result) {
+ fprintf(stderr, "Error %s: %s\n", message, error ? error->message : "");
+ exit(EXIT_FAILURE);
+ }
+}
+
+
+/**
+ * Load a photo from disk into an EContactPhoto record.
+ *
+ * @param mime_type MIME type to store in the photo record.
+ * @param filename Filename on disk to load.
+ */
+EContactPhoto load_photo(char* mime_type, char* filename) {
+ struct stat stbuf;
+ stat(filename, &stbuf);
+ char* photo_data = malloc(stbuf.st_size);
+ try(photo_data != NULL, "allocating memory for photo");
+ FILE *file = fopen(filename, "r");
+ try(file != NULL, "opening file");
+ int read;
+ char* current = photo_data;
+ while ((read = fread(current, 1, stbuf.st_size, file)) > 0) {
+ current += read;
+ }
+ fclose(file);
+
+ EContactPhoto photo;
+ photo.type = E_CONTACT_PHOTO_TYPE_INLINED;
+ photo.data.inlined.mime_type = mime_type;
+ photo.data.inlined.length = stbuf.st_size;
+ photo.data.inlined.data = photo_data;
+
+ return photo;
+}
+
+
+/**
+ * Create a date record corresponding to the given strings.
+ *
+ * @param year Character representation of the year.
+ * @param month Character representation of the month.
+ * @param day Character representation of the day.
+ */
+EContactDate read_date(char* year, char* month, char* day) {
+ EContactDate date;
+ date.year = atoi(year);
+ date.month = atoi(month);
+ date.day = atoi(day);
+ return date;
+}
+
+
+/**
+ * Main entry point: do the work of updating matched records.
+ */
+int main(int argc, char* argv[]) {
+ g_type_init();
+
+ try(argc > 2, "\nsyntax: <name> --photo <mime-type> <file>\n <name> --birthday <year> <month> <day>");
+
+ // -- Data structures we'll use later...
+ //
+ gboolean is_photo;
+ gboolean is_birthday;
+ EContactPhoto photo;
+ EContactDate birthday;
+ if (strcmp(argv[2], "--photo") == 0) {
+ try(argc == 5, "syntax: <name> --photo <mime-type> <file>");
+ photo = load_photo(argv[3], argv[4]);
+ is_photo = 1;
+
+ } else if (strcmp(argv[2], "--birthday") == 0) {
+ try(argc == 6, "syntax: <name> --birthday <year> <month> <day>");
+ birthday = read_date(argv[3], argv[4], argv[5]);
+ is_birthday = 1;
+
+ } else {
+ try(1 == 0, "\nsyntax: <name> --photo <mime-type> <file>\n <name> --birthday <year> <month> <day>");
+ }
+
+ // -- Open the address book...
+ //
+ EBook *book = e_book_new_system_addressbook(&error);
+ try(book != NULL, "finding address book");
+ try(e_book_open(book, FALSE, &error), "opening address book");
+ try(e_book_is_writable(book), "book is writable");
+
+ // -- Find the list of contacts...
+ //
+ GList *contacts = NULL;
+ EBookQuery *query = e_book_query_field_test(E_CONTACT_FULL_NAME, E_BOOK_QUERY_IS, argv[1]);
+ try(e_book_get_contacts(book, query, &contacts, &error), "listing contacts");
+ e_book_query_unref(query);
+
+ // -- Update the contacts...
+ //
+ int i = 0;
+ for (GList* node = g_list_first(contacts); node != NULL; node = g_list_next(node)) {
+ EContact *contact = E_CONTACT(node->data);
+ if (is_photo) e_contact_set(contact, E_CONTACT_PHOTO, &photo);
+ if (is_birthday) e_contact_set(contact, E_CONTACT_BIRTH_DATE, &birthday);
+
+ if (is_photo || is_birthday) {
+ try(e_book_commit_contact(book, contact, &error), "committing contact");
+ i++;
+ }
+ }
+ printf("Modified %d records\n", i);
+
+ return EXIT_SUCCESS;
+}
+
--- /dev/null
+import os
+import os.path
+import urllib
+import Image
+import ImageOps
+
+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.temp_file = os.tmpnam()
+ self.book = book
+
+
+ # -----------------------------------------------------------------------
+ def close(self):
+ """Close the store and tidy-up any resources."""
+
+ if (os.path.isfile(self.temp_file)):
+ os.unlink(self.temp_file)
+
+
+ # -----------------------------------------------------------------------
+ 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."""
+
+ urllib.urlretrieve(url, self.temp_file)
+ im = Image.open(self.temp_file)
+ (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))
+ im.save(self.temp_file, "JPEG")
+
+ print "Updating photo for %s" % (contact.get_name())
+ os.spawnl(os.P_WAIT, '/opt/hermes/bin/contact-update', 'contact-update', contact.get_name(), '--photo', 'image/jpeg', self.temp_file)
--- /dev/null
+#!/usr/bin/env python
+
+import gtk, gobject
+import gnome.gconf
+import hildon, osso
+import time
+import thread
+from hermes import Hermes
+
+gobject.threads_init()
+
+class HermesGUI:
+ """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):
+ """Constructor. Initialises the gconf connection."""
+ self.gc = gnome.gconf.client_get_default()
+
+
+ # -----------------------------------------------------------------------
+ def doSync(self, widget, force, main = True):
+ if main and not self.get_use_facebook() and not self.get_use_twitter():
+ saved = self.open_prefs()
+ if saved == gtk.RESPONSE_DELETE_EVENT:
+ return
+
+ if main:
+ self.window.set_property('sensitive', False)
+ thread.start_new_thread(self.doSync, (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())
+ fb2c.load_friends()
+ fb2c.sync_contacts(resync = force)
+ gobject.idle_add(hildon.hildon_banner_show_information, self.window, '', "Updated %d contacts" % (len(fb2c.updated)))
+ gobject.idle_add(self.window.set_property, 'sensitive', True)
+ except Exception, e:
+ print e
+ gobject.idle_add(self.report_error, 'Something went wrong: ' + e.message)
+
+
+ # -----------------------------------------------------------------------
+ def open_prefs(self, widget = None):
+ dialog = gtk.Dialog('Accounts', self.window)
+ dialog.add_button('Save', gtk.RESPONSE_OK)
+
+ use_facebook = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+ use_facebook.set_label('Use Facebook')
+ use_facebook.set_active(self.get_use_facebook())
+ dialog.vbox.add(use_facebook)
+
+ use_twitter = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+ use_twitter.set_label('Use Twitter')
+ use_twitter.set_active(self.get_use_twitter())
+ dialog.vbox.add(use_twitter)
+
+ tw_indent = gtk.HBox()
+ tw_user = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+ tw_user.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL)
+ tw_user.set_placeholder("Username")
+ tw_user.set_property('is-focus', False)
+ tw_user.set_text(self.get_twitter_credentials()[0])
+ self.sync_edit(use_twitter, tw_user)
+ use_twitter.connect('toggled', self.sync_edit, tw_user)
+ tw_indent.pack_start(tw_user, padding=48)
+ dialog.vbox.add(tw_indent)
+
+ tw_indent = gtk.HBox()
+ tw_pass = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+ tw_pass.set_placeholder("Password")
+ tw_pass.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL |
+ gtk.HILDON_GTK_INPUT_MODE_INVISIBLE)
+ tw_pass.set_text(self.get_twitter_credentials()[1])
+ self.sync_edit(use_twitter, tw_pass)
+ use_twitter.connect('toggled', self.sync_edit, tw_pass)
+ tw_indent.pack_start(tw_pass, padding=48)
+ dialog.vbox.add(tw_indent)
+
+ dialog.show_all()
+ result = dialog.run()
+ dialog.hide()
+ if result == gtk.RESPONSE_OK:
+ self.set_use_facebook(use_facebook.get_active())
+ self.set_use_twitter(use_twitter.get_active(), tw_user.get_text(), tw_pass.get_text())
+
+ return result
+
+
+ # -----------------------------------------------------------------------
+ def sync_edit(self, use_twitter, edit):
+ edit.set_property('sensitive', use_twitter.get_active())
+
+
+ # -----------------------------------------------------------------------
+ def need_auth(self, main = False):
+ if main:
+ hildon.hildon_banner_show_information(self.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.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.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):
+ hildon.hildon_banner_show_information(self.window, '', e)
+
+
+ # -----------------------------------------------------------------------
+ def main(self):
+ # -- Window and app...
+ #
+ self.app = hildon.Program()
+ self.window = hildon.Window()
+ gtk.set_application_name('Hermes')
+ osso_context = osso.Context('org.maemo.hermes', '0.0.1', False)
+ self.app.add_window(self.window)
+
+ self.window.connect("delete-event", gtk.main_quit)
+
+ # -- Main window buttons...
+ #
+ box = gtk.HButtonBox()
+ self.window.add(box)
+
+ box.set_property('layout-style', gtk.BUTTONBOX_SPREAD)
+ button = hildon.Button(gtk.HILDON_SIZE_THUMB_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
+ title = 'Retrieve', value = "Get contacts' missing info")
+ button.set_property('width-request', 250)
+ button.connect('clicked', self.doSync, False)
+ box.add(button)
+
+ button = hildon.Button(gtk.HILDON_SIZE_THUMB_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
+ title = 'Update', value = "Update contacts' info")
+ button.set_property('width-request', 250)
+ button.connect('clicked', self.doSync, True)
+ box.add(button)
+
+ # -- Application menu...
+ #
+ menu = hildon.AppMenu()
+ button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+ button.set_label("Accounts")
+ button.connect("clicked", self.open_prefs)
+ menu.append(button)
+ menu.show_all()
+ self.window.set_app_menu(menu)
+
+ self.window.show_all()
+ gtk.main()
+
+
+ def get_use_facebook(self):
+ return self.gc.get_bool("/apps/maemo/hermes/use_facebook")
+
+
+ def set_use_facebook(self, value):
+ self.gc.set_bool("/apps/maemo/hermes/use_facebook", value)
+
+
+ def get_use_twitter(self):
+ return self.gc.get_bool("/apps/maemo/hermes/use_twitter")
+
+
+ def set_use_twitter(self, value, user, password):
+ self.gc.set_bool("/apps/maemo/hermes/use_twitter", value)
+ self.gc.set_string("/apps/maemo/hermes/twitter_user", user)
+ self.gc.set_string("/apps/maemo/hermes/twitter_pwd", password)
+
+
+ def get_twitter_credentials(self):
+ return (self.gc.get_string("/apps/maemo/hermes/twitter_user") or '',
+ self.gc.get_string("/apps/maemo/hermes/twitter_pwd") or '')
+
+
+# -------------------------------------------------------------------------
+if __name__ == '__main__':
+ HermesGUI().main()
+
--- /dev/null
+import os.path
+import evolution
+from facebook import Facebook
+import twitter
+import gnome.gconf
+from contacts import ContactStore
+import names
+
+class Hermes:
+ """Encapsulate the process of syncing Facebook friends' information with the
+ Evolution contacts' database. This should be used as follows:
+
+ * Initialise, passing in a callback (methods: need_auth(),
+ block_for_auth(), use_twitter(), use_facebook()).
+ * Call load_friends().
+ * Call sync_contacts().
+ * Retrieve information on changes effected.
+
+ 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> 2009.
+ Released under the Artistic Licence."""
+
+
+ # -----------------------------------------------------------------------
+ def __init__(self, callback, twitter = None, facebook = False):
+ """Constructor. Passed a callback which must implement three informational
+ methods:
+
+ need_auth() - called to indicate a 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.
+
+ Other parameters:
+ twitter - a username/password tuple or None if Twitter should not be
+ used. Defaults to None.
+
+ facebook - boolean indicating if Facebook should be used. Defaults to
+ False.
+ """
+
+ self.gc = gnome.gconf.client_get_default()
+ self.callback = callback
+ self.twitter = twitter
+ self.facebook = facebook
+
+ # -- 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.')
+
+ # -- Get private keys for this app...
+ #
+ 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
+
+
+ # -----------------------------------------------------------------------
+ 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
+
+ self.callback.need_auth()
+ self.fb.auth.createToken()
+ self.fb.login()
+ self.callback.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_int('/apps/maemo/hermes/uid', session['uid'])
+
+
+ # -----------------------------------------------------------------------
+ def load_friends(self):
+ """Load information on the authenticated user's friends. If no user is
+ currently authenticated, prompts for a login."""
+
+ self.friends = {}
+ self.blocked_pictures = []
+
+ # -- Get a user session and retrieve Facebook friends...
+ #
+ if self.facebook:
+ 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_int('/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.callback.progress(0, 0)
+ # Get the list of friends...
+ attrs = ['uid', 'name', 'pic_big', 'birthday_date']
+ for friend in self.fb.users.getInfo(self.fb.friends.get(), attrs):
+ friend['pic'] = friend[attrs[2]]
+ self.friends[friend['name']] = friend
+ if not friend['pic']:
+ self.blocked_pictures.append(friend)
+
+ # -- Retrieve following information from Twitter...
+ #
+ if self.twitter is not None:
+ (user, passwd) = self.twitter
+ api = twitter.Api(username=user, password=passwd)
+ users = api.GetFriends()
+ for friend in api.GetFriends():
+ self.friends[friend.name] = {'name': friend.name, 'pic': friend.profile_image_url, 'birthday_date': None}
+
+
+ # -----------------------------------------------------------------------
+ def sync_contacts(self, resync = False):
+ """Synchronise Facebook profiles to contact database. If resync is false,
+ no existing information will be overwritten."""
+
+ # -- Find addresses...
+ #
+ print "+++ Syncing contacts..."
+ addresses = evolution.ebook.open_addressbook('default')
+ print "+++ Addressbook opened..."
+ store = ContactStore(addresses)
+ print "+++ Contact store created..."
+ self.updated = []
+ contacts = addresses.get_all_contacts()
+ current = 0
+ maximum = len(contacts)
+ for contact in contacts:
+ current += 1
+ self.callback.progress(current, maximum)
+ for name in names.variants(contact.get_name()):
+ if name in self.friends:
+ friend = self.friends[name]
+
+ if friend['pic'] and (resync or contact.get_property('photo') is None):
+ print "Picture for %s is [%s]" % (name, friend['pic'])
+ store.set_photo(contact, friend['pic'])
+ self.updated.append(contact)
+
+ if friend['birthday_date'] and (resync or contact.get_property('birth-date') is None):
+ print "Birthday for %s is [%s]" % (name, friend['birthday_date'])
+
+ break
+
+ store.close()
+
--- /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
+
+ 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
+