Add v0.0.1 of Hermes from source tarball 0.0.1
authorAndrew Flegg <andrew@bleb.org>
Tue, 13 Oct 2009 19:31:45 +0000 (20:31 +0100)
committerAndrew Flegg <andrew@bleb.org>
Tue, 13 Oct 2009 19:31:45 +0000 (20:31 +0100)
22 files changed:
package/Makefile [new file with mode: 0644]
package/debian/README [new file with mode: 0644]
package/debian/README.Debian [new file with mode: 0644]
package/debian/changelog [new file with mode: 0644]
package/debian/compat [new file with mode: 0644]
package/debian/control [new file with mode: 0644]
package/debian/copyright [new file with mode: 0644]
package/debian/dirs [new file with mode: 0644]
package/debian/docs [new file with mode: 0644]
package/debian/hermes.postinst [new file with mode: 0644]
package/debian/rules [new file with mode: 0755]
package/share/hermes-256.png [new file with mode: 0644]
package/share/hermes-48.png [new file with mode: 0644]
package/share/hermes-64.png [new file with mode: 0644]
package/share/hermes.desktop [new file with mode: 0644]
package/share/hermes.service [new file with mode: 0644]
package/src/console.py [new file with mode: 0644]
package/src/contact-update.c [new file with mode: 0644]
package/src/contacts.py [new file with mode: 0644]
package/src/gui.py [new file with mode: 0755]
package/src/hermes.py [new file with mode: 0644]
package/src/names.py [new file with mode: 0644]

diff --git a/package/Makefile b/package/Makefile
new file mode 100644 (file)
index 0000000..3a74903
--- /dev/null
@@ -0,0 +1,21 @@
+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 {} \;
diff --git a/package/debian/README b/package/debian/README
new file mode 100644 (file)
index 0000000..08a6271
--- /dev/null
@@ -0,0 +1,6 @@
+The Debian Package hermes
+----------------------------
+
+Comments regarding the Package
+
+ -- unknown <andrew@bleb.org>  Wed, 30 Sep 2009 21:21:27 +0100
diff --git a/package/debian/README.Debian b/package/debian/README.Debian
new file mode 100644 (file)
index 0000000..e8c6a83
--- /dev/null
@@ -0,0 +1,6 @@
+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
diff --git a/package/debian/changelog b/package/debian/changelog
new file mode 100644 (file)
index 0000000..0d7ee2a
--- /dev/null
@@ -0,0 +1,5 @@
+hermes (0.0.1) unstable; urgency=low
+
+  * Initial Release.
+
+ -- unknown <andrew@bleb.org>  Wed, 30 Sep 2009 21:21:27 +0100
diff --git a/package/debian/compat b/package/debian/compat
new file mode 100644 (file)
index 0000000..7ed6ff8
--- /dev/null
@@ -0,0 +1 @@
+5
diff --git a/package/debian/control b/package/debian/control
new file mode 100644 (file)
index 0000000..ca4f355
--- /dev/null
@@ -0,0 +1,83 @@
+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:
+ iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABmJLR0QA/wD/
+ AP+gvaeTAAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAwAAAAMADO
+ 7oxXAAAKAklEQVRo3u1Z229U9xH+Zn7nujd711eMsUPAGAKhIJQmgTRVCVFU
+ tWmiiL7kpVKbl/apD/0HKiVSH5r3Ks9tHxuJoqqqmqS0uUAChBDuxoBNYmMv
+ rNe33T3nd5k+GIyN16gSKCiqRxrtnqPZOd83M2dmzlkSEXybhR81gDUCjxrA
+ GoFHDeD/noD35z/+btmJOJPF089+nztLrcJkV/RYEQC08J2cB442PBAAsfNw
+ pgyQQARgam5XSxxfuXZVzn5xQgR3YXnnzh5dPMhmC/Tqq6/HpULcKpLMWzM/
+ 3dQb3+bgggcPodMQO71IwDYZS0KKQj/bvam/N7147nj1y9OfGefsAoGtnR2L
+ hs+8+Fq2c/2GXqV4G7O6IVDHRKx7cJT/oxAWs7tUFEd5AT/rKTV94MCPzrQq
+ lCeuD1sA8DqCaNHQD/xiGPrPMNP3ALoGCs9AarPfGIHVWFGwiSEvi6LxOAr9
+ rB/84w5ub0KFAIBMLk9d6/vbPUUvMfMugDY5F/wVqJ8Cvpl1o2n5kx8Q8Byx
+ 2qtgJ0Wo3rX1O59cHh+rAoA3NDIMANi+Yw8z0RZm3kkU9Am4DUj2EQXnIEl6
+ r9+HSkkAWuXmBcfrQLSfKNoAdiUFM1YsltovjVypEgTsBxE8P8Lg1idjkGwB
+ hd1C+RiUaSXi/aCwW4Rwrz7spDS7hsBngPcQ+d8RzoXgfCsRDwZh2L5ly5PK
+ DyJw2qhDJ3WYJGlhok1EcZ44Q0QZn+HvJnh7AJ8hjOWqHiL8e30vKCEqkvB+
+ oqibKCJQVhH8Dk95g7X5GUobdTAJo9TaQd3dGxxD9YMiBSiAFEBRNwkfYMRF
+ OMIKFXpw7ACa+SaniCh4gsnbB8pEAIEoAFGQJ6Hi9m27FBwvdPSBwV1eqbW4
+ leAVQP7t6UwAZyIibx/gP8miiB1jqZJ7CAQEuNcvOwZRlCXhF0DhRnBwZ3QC
+ CAOP1cbBrTvbsrkicTbTgigTKxEpAapr2XbBPoHCjQQ+QBQXVkbqYWwizTKr
+ QBQOENR+cJxb1p/IU3DoJaFCe1cneGDTDm9daV0u9MIdRGEBxMudqzhHUPtB
+ wTaIR3CMu/pwMrDcJ4MQZiB8ABRsA4fLbzbymFU46Cuva8fmXZF34cwZu7F/
+ QEi4BxSEK7oxBwwOt8LUf/jvo6f7T5+/NOisC0XEpVqLoUDy+Rw3GolyzpG1
+ RgCyAEyxWDTGGMzNzSkAXpqmvu/7XpomKk01B0FAUaBIdMKAkBNBV3vb0Os/
+ fe2zbE69BBUXlwcUAPlEUAVP+W3Xroxqb31PH7fkWzwIuabdmBjw4pZPPzn2
+ xu//8Kee2VoKgMBMoCXmzrkVxyICJw6Q2133zvM3LUTeiQOBQExQyoNiBU8R
+ iP0Lb/z8Zx3wwuatThSxMHV0dHheJlugytSM399HjQUSTUQFfOni5XU6MRAw
+ RARjI0No1OawZDm9mzRmZDIZ+L6PJElQr9dxv5cHYZRFR89jSCWFImDo4uUB
+ +FlaEf27ZZc06qkY7RRfPnvetmRyloTykFWeD4jAxlJ/qRVdrQV4zAtBFAFE
+ IPeo7/vYv38/3n77bTz++ONQSq2wWaZwEGegnENBEQJrFDy/ORYRQCgOlO/b
+ emK51NpOhULROYtRGFPDKpHS9QQxGBkA2TBYVi73CjODiNDT04OWlhZ4nndf
+ e8hC78soRp4VzHwdq+GAcwLjxiFqqq9vE3njo1+7ybHxmWK+5QICMwPnWqBW
+ ll5Sa8BXPkIhRJ4D3weQUgr5fB5aazjn0N3djUqlgrm5OVhrFyNvjIG1Fo1G
+ gkatAdEpdKOOgJ5enazWVrT72ib66umTn6eessDwuYt6YOPANWg7DGPWQ6kV
+ 6bPGwgnBOYFiBuH+BNra2lCtVnHw4EF0d3fjnXfewaFDh1Cv1+Gcg3N3bzci
+ AtOCRwaQpBriBKTuuYYIoM28aPulSXR19OJlx2wJl8+cs1bb66LNWaQ6hWuS
+ PgcYAA1jYAFYa5GmKWq1GmZnZ1GtVnHz5k1MTExgdHQUWmuUy2UcPnwYU1NT
+ 6OvrQ5qmMMYsA79YdsRQxPCJIU7QdFs0RqDNvNP22NDZ87O2nornVIBa4uTU
+ 8ROVvc8/P0KJrsBLuxEFvLSr1ms1/PM/72GqNoeZ+VnUk0bTzkJEUEqhs7MT
+ x48fx8mTJ/HKK6+gUqmsXha3W6kHQgiC36yXWAs00hTaDjVqjfKpE1+kTgVg
+ TjUoSZAvtMIZdxLGnkU9mUGqBdYBbqHTJMZiYuwrVKcr0EbjfhKGITo7OzE+
+ Pg7nHAqFAsbGxmCtbQ6fCJ5SUMpDpAKE7C8kQARwDjAWqCcGDT0J4z4l4q+y
+ cQxONVgsQML41+G/p9a4S9DuAyT6PGpJDfXEIkkFqRFmRoYUYlJg4lW7ChGh
+ UCigUCigXC6jtbUVcRwvklntN34QIAgD5MIIohTIWEGiBQ3tUE8M6voWtDkq
+ Vo4MX7h0a+TisBMLeHfG0HR1Wk59duLGd/ftfZ8EMcjkYGUdmGMQFCsvUF7A
+ GQgsWxjXPJoAUCqVoJTC+Pg4ent7Ya1FuVxedZgREYIwRJY8BOSBw0BQ1xoQ
+ AycaztWgzZei7bvzMzOnTxw90VhwRfC6unoXHV27cEVv2jxwttjeFiviPAS7
+ Qa4dhECYein0Y0VABg61NGkKRkQwOjqKN998E1evXsXU1BTeeustlMvl1e8A
+ IsRhiJg8MDHifNYiNTcgUofIDEQmoe17c9MzH40MX5uQ1Mkd3N7oyLVFR0yE
+ 619dn//lb359Lut5CpYmAVkPokKUzTwf5nP9qUkRiCBMmg0zAhFQqVRw5MgR
+ iAATE5OYnJxcnNDLbBc+4PsB4iAEMwNgFNtLczDmMwDzEFyHyNfOuo+PHfv0
+ xukPj6aVanXRixeF0TIItdk5OfLe+1M/ePGFk3EuNw2RNhDantq9e+bd9V2/
+ mp2vkRVBS1sJjuQuiSWv7IgWonpHbxssbAEid3e6JfwZBAYhn81i965dJ+Dk
+ EAQpgFFnTWV2bnb084+P6qTewFLM9NuDv2iSUuDlH/+Edu59KkOKI2JqFcA/
+ c+nCUxeGh/rmanM6aSQNcqK5sxUAoBST1oaUYlhrKUlSxHEMrVMCQGEYkjUW
+ fuATALo9yYkSQ9TQij2OrLHRhp7eW8/tefpD3/dvinMGgspMuZr+7d2/JEOX
+ h1dg9ZrVpHOC/k2bpXbp2nzdk1rQVppRxLSlf+PYjs2DEGOT6sTNFAQp7duJ
+ B5LqPEY+Ool1j/UpPxOFEPGnp6d1LsroiZGrUlChCbNFlNo7gCYE6NQHH604
+ KSJ4Yvt2UJrCiIURAYGRaINcPks+kaTGghQh7Cw9GIHUYPZGGdlcDk5r1FNN
+ c7WadLe1oXKrjEIuD/Yj3CjfwM3yzZUE1v7kWyOwRuDbLWsEHrWsEXjUskbg
+ Uct/AQWa/LNlD1YMAAAAJXRFWHRjcmVhdGUtZGF0ZQAyMDA5LTA5LTMwVDIx
+ OjExOjE5KzAxOjAwUrmhuQAAACV0RVh0bW9kaWZ5LWRhdGUAMjAwOS0wOS0z
+ MFQyMToxMToxOSswMTowMA0I140AAAAZdEVYdFNvZnR3YXJlAEFkb2JlIElt
+ YWdlUmVhZHlxyWU8AAAAAElFTkSuQmCC
+
diff --git a/package/debian/copyright b/package/debian/copyright
new file mode 100644 (file)
index 0000000..a303810
--- /dev/null
@@ -0,0 +1,26 @@
+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'.
diff --git a/package/debian/dirs b/package/debian/dirs
new file mode 100644 (file)
index 0000000..ca882bb
--- /dev/null
@@ -0,0 +1,2 @@
+usr/bin
+usr/sbin
diff --git a/package/debian/docs b/package/debian/docs
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/package/debian/hermes.postinst b/package/debian/hermes.postinst
new file mode 100644 (file)
index 0000000..411bd18
--- /dev/null
@@ -0,0 +1,10 @@
+#!/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
diff --git a/package/debian/rules b/package/debian/rules
new file mode 100755 (executable)
index 0000000..0cc4446
--- /dev/null
@@ -0,0 +1,98 @@
+#!/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
diff --git a/package/share/hermes-256.png b/package/share/hermes-256.png
new file mode 100644 (file)
index 0000000..0f7f74b
Binary files /dev/null and b/package/share/hermes-256.png differ
diff --git a/package/share/hermes-48.png b/package/share/hermes-48.png
new file mode 100644 (file)
index 0000000..b33674e
Binary files /dev/null and b/package/share/hermes-48.png differ
diff --git a/package/share/hermes-64.png b/package/share/hermes-64.png
new file mode 100644 (file)
index 0000000..083d453
Binary files /dev/null and b/package/share/hermes-64.png differ
diff --git a/package/share/hermes.desktop b/package/share/hermes.desktop
new file mode 100644 (file)
index 0000000..0852d3a
--- /dev/null
@@ -0,0 +1,8 @@
+[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
diff --git a/package/share/hermes.service b/package/share/hermes.service
new file mode 100644 (file)
index 0000000..95bbb30
--- /dev/null
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.maemo.hermes
+Exec=/opt/hermes/bin/hermes
diff --git a/package/src/console.py b/package/src/console.py
new file mode 100644 (file)
index 0000000..5f8cd84
--- /dev/null
@@ -0,0 +1,20 @@
+class ConsoleUICallback:
+  """Meets the fb2contacts' authentication callback contract using
+     the console.
+
+     Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
+     Released under the Artistic Licence."""
+
+  # -----------------------------------------------------------------------
+  def need_auth(self):
+    print 'Need authentication...'
+
+  # -----------------------------------------------------------------------
+  def block_for_auth(self):
+    print 'Press enter when logged in...'
+    raw_input()
+
+  # -----------------------------------------------------------------------
+  def progress(self, current, maximum):
+    print current, maximum
+
diff --git a/package/src/contact-update.c b/package/src/contact-update.c
new file mode 100644 (file)
index 0000000..47b6a74
--- /dev/null
@@ -0,0 +1,147 @@
+/**
+ * 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;
+}
+
diff --git a/package/src/contacts.py b/package/src/contacts.py
new file mode 100644 (file)
index 0000000..0b7a1e9
--- /dev/null
@@ -0,0 +1,47 @@
+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)
diff --git a/package/src/gui.py b/package/src/gui.py
new file mode 100755 (executable)
index 0000000..ae572da
--- /dev/null
@@ -0,0 +1,232 @@
+#!/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()
+
diff --git a/package/src/hermes.py b/package/src/hermes.py
new file mode 100644 (file)
index 0000000..f2178ee
--- /dev/null
@@ -0,0 +1,170 @@
+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()
+
diff --git a/package/src/names.py b/package/src/names.py
new file mode 100644 (file)
index 0000000..b0b5fd0
--- /dev/null
@@ -0,0 +1,43 @@
+"""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
+