the requesting of additional permissions when authorising the account.
Fixes MB#11103.
Depends: ${shlibs:Depends}, ${misc:Depends}, python-imaging |
python2.5-imaging, python-osso | python2.5-osso, python-hildon |
python2.5-hildon, python-simplejson, python-evolution, python-conic,
- python-facebook (>= 0.svn20090225-0maemo2), python-evolution,
- python-gobject (>= 2.16), gnome-python
+ python-evolution, python-gobject (>= 2.16), gnome-python
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
#!/bin/sh
set -e
-gconftool-2 -s /apps/maemo/hermes/facebook_app 5916f12942feea4b3247d42a84371112 --type string
+gconftool-2 -s /apps/maemo/hermes/facebook_key 5916f12942feea4b3247d42a84371112 --type string
gconftool-2 -s /apps/maemo/hermes/facebook_secret 19f7538edd96b6870f2da7e84a6390a4 --type string
gconftool-2 -s /apps/maemo/hermes/gravatar_email maemohermes@wendt.se --type string
gconftool-2 -s /apps/maemo/hermes/gravatar_key b14ec179822b --type string
--- /dev/null
+import webbrowser
+import urllib, urllib2
+from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+import urlparse, cgi
+
+class OAuth2:
+ """Implementation of OAuth2 protocol, as described by Facebook:
+
+ http://developers.facebook.com/docs/authentication/#web_server_auth
+
+ Uses a local web server to retrieve the code and hence the access token.
+
+ Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+ Released under the Artistic Licence."""
+
+
+ # -----------------------------------------------------------------------
+ def __init__(self, client_id, client_secret, access_token = None):
+ '''Create a new OAuth 2.0 client, with the folowing arguments:
+
+ client_id - Identifier of the application.
+ client_secret - Secret API key of the application.
+ access_token - Current access token, if any.'''
+
+ self._client_id = client_id
+ self._client_secret = client_secret
+ self._access_token = access_token
+
+
+ # -----------------------------------------------------------------------
+ def authorise(self, authorise_url, access_token_url, args = None):
+ '''Open a browser window to allow the user to log in and
+ authorise this client.'''
+
+ redirect_uri = 'http://localhost:3435/success'
+ webbrowser.open_new('%s?client_id=%s&redirect_uri=%s&%s' % (authorise_url, self._client_id, redirect_uri, args and urllib.urlencode(args) or ''))
+ handler = OAuthCodeHandler()
+ code = handler.run()
+
+ result = urllib2.urlopen('%s?client_id=%s&redirect_uri=%s&client_secret=%s&code=%s' % (access_token_url, self._client_id, redirect_uri, self._client_secret, code)).read()
+ params = cgi.parse_qs(result)
+ if 'access_token' in params:
+ self._access_token = params['access_token'][0]
+ else:
+ print result, params
+ raise Exception('Unable to retrieve access_token from Facebook')
+
+
+ # -----------------------------------------------------------------------
+ def request(self, url, args = None):
+ '''Make an authenticated request to the given URL. If no
+ access token is currently set, an exception will be thrown.
+
+ An optional dictionary of parameters can be specified.'''
+
+ if not self._access_token:
+ raise Exception("Unauthorised")
+
+ query_url = '%s?access_token=%s&%s' % (url, self._access_token, args and urllib.urlencode(args) or '')
+# print query_url
+ result = urllib2.urlopen(query_url).read()
+ return result
+
+
+ # -----------------------------------------------------------------------
+ def get_access_token(self):
+ """Get the access token in use by this OAuth 2.0 client,
+ so that it can be persisted."""
+
+ return self._access_token
+
+
+# ---------------------------------------------------------------------------
+class OAuthCodeHandler(HTTPServer):
+ """Handles the response from an OAuth2 handler and allows the
+ retrieval of the code."""
+
+ # -----------------------------------------------------------------------
+ def __init__(self, success_response = '<h1>Success</h1><p>Please now close this window.</p>',
+ failure_response = '<h1>Failed</h1><p>%s</p>'):
+ '''Create a new handler, with optional overrides of the
+ success and failure response messages.'''
+
+ HTTPServer.__init__(self, ('127.0.0.1', 3435), OAuthHttpRequestHandler)
+ self._success_response = success_response
+ self._failure_response = failure_response
+ self._code = None
+
+
+ # -----------------------------------------------------------------------
+ def run(self):
+ '''Start a server and wait for a redirect. Return the code
+ if/when successful.'''
+
+ self.handle_request()
+ return self._code
+
+
+ # -----------------------------------------------------------------------
+ def set_code(self, code):
+ '''Called by the handler to feed us back the code.'''
+
+ self._code = code
+
+ def get_success_response(self):
+ return self._success_response
+
+ def get_failure_response(self):
+ return self._failure_response
+
+
+# ---------------------------------------------------------------------------
+class OAuthHttpRequestHandler(BaseHTTPRequestHandler):
+ def do_GET(self):
+ qs = urlparse.urlparse(self.path).query
+ params = cgi.parse_qs(qs)
+
+ if 'code' in params:
+ self.server.set_code(params['code'][0])
+
+ self.send_response(200)
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+ self.wfile.write(self.server.get_success_response())
+ else:
+ print qs, params
+ self.send_response(500)
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+ self.wfile.write(self.server.get_failure_response())
+
+ return
--- /dev/null
+import urllib, urllib2
+import simplejson
+
+class FacebookApi():
+ """Facebook backend for Hermes, using the Graph API:
+
+ http://developers.facebook.com/docs/reference/api/
+
+ Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+ Released under the Artistic Licence."""
+
+
+ # -----------------------------------------------------------------------
+ def __init__(self, oauth):
+ self._oauth = oauth
+
+
+ # -----------------------------------------------------------------------
+ def authenticate(self):
+ '''Authenticate the user with Facebook.'''
+
+ self._oauth.authorise('https://graph.facebook.com/oauth/authorize',
+ 'https://graph.facebook.com/oauth/access_token',
+ {'scope': 'user_about_me,friends_about_me,user_birthday,friends_birthday,user_website,friends_website,user_work_history,friends_work_history'})
+
+
+ # -----------------------------------------------------------------------
+ def get_user(self):
+ '''Return the name of the authenticated user.'''
+
+ data = self._request('https://graph.facebook.com/me')
+ return data['name']
+
+
+ # -----------------------------------------------------------------------
+ def get_friends(self):
+ '''Return the full list of people being followed by the user.
+
+ The result is a list of users:
+ http://developers.facebook.com/docs/reference/api/user/'''
+
+ def copy(data, from_key, to, to_key = None):
+ if not to_key:
+ to_key = from_key
+
+ if from_key in data:
+ to[to_key] = data[from_key]
+
+ users = self._request('https://graph.facebook.com/me/friends', {'fields': 'id,name,link,birthday,website,picture', 'type': 'large'})
+ return users['data']
+
+
+ # -----------------------------------------------------------------------
+ def _request(self, url, args = None):
+ """Make an authenticated request to Facebook and check the
+ JSON response. Return the dictionary if no errors."""
+
+ json = self._oauth.request(url, args)
+# print json
+ data = simplejson.loads(json)
+ if 'error' in data:
+ raise Exception(data['error'])
+
+ return data
-from pythonfacebook import Facebook, FacebookError
import gnome.gconf
import gtk, hildon
import org.maemo.hermes.engine.provider
-import org.maemo.hermes.engine.facebook.service
+from org.maemo.hermes.engine.facebook.service import Service
+from org.maemo.hermes.engine.facebook.api import FacebookApi
+import oauth2
class Provider(org.maemo.hermes.engine.provider.Provider):
"""Facebook provider for Hermes.
self._gc = gnome.gconf.client_get_default()
- key_app = self._gc.get_string('/apps/maemo/hermes/facebook_app')
+ key_app = self._gc.get_string('/apps/maemo/hermes/facebook_key')
key_secret = self._gc.get_string('/apps/maemo/hermes/facebook_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
-
- if self.fb.session_key is None:
- self.fb.session_key = self._gc.get_string('/apps/maemo/hermes/facebook_session_key')
- self.fb.secret = self._gc.get_string('/apps/maemo/hermes/facebook_secret_key')
- self.fb.uid = self._gc.get_string('/apps/maemo/hermes/facebook_uid')
+ access_token = self._gc.get_string('/apps/maemo/hermes/facebook_access_token')
+ self.oauth = oauth2.OAuth2(key_app, key_secret, access_token)
+ self.api = FacebookApi(self.oauth)
# -----------------------------------------------------------------------
def get_account_detail(self):
"""Return the email address associated with the user, if available."""
- name = self._gc.get_string('/apps/maemo/hermes/facebook_user')
- return name and name or _('Pending authorisation')
+ return self._gc.get_string('/apps/maemo/hermes/facebook_user')
# -----------------------------------------------------------------------
dialog = gtk.Dialog(self.get_name(), parent)
dialog.add_button(_('Disable'), gtk.RESPONSE_NO)
- dialog.add_button(_('Enable'), gtk.RESPONSE_YES)
+ enable = dialog.add_button(_('Enable'), gtk.RESPONSE_YES)
+
+ button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
+ hildon.BUTTON_ARRANGEMENT_VERTICAL)
+ self._handle_button(None, button, enable)
+ button.connect('clicked', self._handle_button, button, enable)
+ dialog.vbox.add(button)
checkbox = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
checkbox.set_label(_('Create birthday-only contacts'))
checkbox.set_active(self._gc.get_bool('/apps/maemo/hermes/facebook_birthday_only'))
dialog.vbox.add(checkbox)
- dialog.vbox.add(gtk.Label("\n" + _('Note: authentication via web page') + "\n"))
-
- clear = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
- hildon.BUTTON_ARRANGEMENT_VERTICAL,
- title = _("Clear authorisation"))
- clear.connect('clicked', self._clear_auth)
- dialog.vbox.add(clear)
+ dialog.vbox.add(gtk.Label(""))
dialog.show_all()
result = dialog.run()
# -----------------------------------------------------------------------
- def _clear_auth(self, event):
- """Clear Facebook authorisation information. Triggered by pressing
- the 'clear' button in the preferences dialogue."""
+ def _handle_button(self, e, button, enable):
+ """Ensure the button state is correct."""
+
+ authenticated = self._gc.get_string('/apps/maemo/hermes/facebook_access_token') is not None
+ if e is not None:
+ if authenticated:
+ self._gc.unset('/apps/maemo/hermes/facebook_access_token')
+ else:
+ self.api.authenticate()
+ self._gc.set_string('/apps/maemo/hermes/facebook_access_token', self.oauth.get_access_token())
+ self._gc.set_string('/apps/maemo/hermes/facebook_user', self.api.get_user())
- self._gc.unset('/apps/maemo/hermes/facebook_session_key')
- self._gc.unset('/apps/maemo/hermes/facebook_secret_key')
- self._gc.unset('/apps/maemo/hermes/facebook_uid')
- self._gc.unset('/apps/maemo/hermes/facebook_user')
+ authenticated = self._gc.get_string('/apps/maemo/hermes/facebook_access_token') is not None
+
+ button.set_title(authenticated and _("Clear authorisation") or _("Authorise"))
+ enable.set_sensitive(authenticated)
# -----------------------------------------------------------------------
"""Return a service instance."""
self._gui = gui_callback
-
- # 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()
-
- return org.maemo.hermes.engine.facebook.service.Service(self.get_id(), self.fb, self._gc.get_bool('/apps/maemo/hermes/facebook_birthday_only'))
-
-
- # -----------------------------------------------------------------------
- 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/facebook_session_key', session['session_key'])
- self._gc.set_string('/apps/maemo/hermes/facebook_secret_key', session['secret'])
- self._gc.set_string('/apps/maemo/hermes/facebook_uid', str(session['uid']))
-
- info = self.fb.users.getInfo([self.fb.uid], ['name'])
- self._gc.set_string('/apps/maemo/hermes/facebook_user', info[0]['name'])
-
+ return Service(self.get_id(), self.api, self._gc.get_bool('/apps/maemo/hermes/facebook_birthday_only'))
friends = []
if self._create_birthday_only:
for friend in self._friends_without_contact:
- friends.append(friend)
+ if friend.is_interesting():
+ friends.append(friend)
return friends
if key in data and data[key]:
callback(data[key])
- friends_data = self._get_friends_data()
+ friends_data = self.fb.get_friends()
for data in friends_data:
friend = Friend(data['name'])
- if 'profile_url' not in data:
- data['profile_url'] = "http://www.facebook.com/profile.php?id=" + str(data['uid'])
+ if 'link' not in data:
+ data['link'] = "http://www.facebook.com/profile.php?id=" + str(data['id'])
if_defined(data, 'website', friend.add_url)
- if_defined(data, 'profile_url', friend.add_url)
- if_defined(data, 'birthday_date', friend.set_birthday_date)
+ if_defined(data, 'link', friend.add_url)
+ if_defined(data, 'birthday', friend.set_birthday_date)
- if_defined(data, 'pic_big', friend.set_photo_url)
+ if_defined(data, 'picture', friend.set_photo_url)
- url = data['profile_url']
+ url = data['link']
friend.add_url(url)
self._register_friend(friend)
self._friends_without_contact.discard(friend)
self._friends_by_contact[contact] = friend
self._contacts_by_friend[friend] = contact
-
-
- # -----------------------------------------------------------------------
- def _get_friends_data(self):
- """Returns a list of dicts, where each dict represents the raw data
- of a friend"""
-
- return self.fb.users.getInfo(self.fb.friends.get(), Service.attrs)
import unittest
-from unit.test_facebook import TestFacebookService
+from unit.test_facebook import TestFacebookService, TestFacebookAPI
from unit.test_gravatar import TestGravatarService
from unit.test_linkedin import TestLinkedInService
from unit.test_twitter import TestTwitterService
from integration.test_gravatar import IntegrationTestGravatarService
from integration.test_linkedinapi import IntegrationTestLinkedInApi
from integration.test_twitter import IntegrationTestTwitterService
+from integration.test_oauth2 import IntegrationTestOAuth2
+from integration.test_facebook import IntegrationTestFacebook
+
if __name__ == '__main__':
unittest.main()
--- /dev/null
+import unittest
+import oauth2
+from org.maemo.hermes.engine.facebook.api import FacebookApi
+import httplib
+httplib.HTTPConnection.debuglevel = 1
+
+class IntegrationTestFacebook(unittest.TestCase):
+ access_token = None
+
+ # -----------------------------------------------------------------------
+ def setUp(self):
+ self.oauth = oauth2.OAuth2('5916f12942feea4b3247d42a84371112', '19f7538edd96b6870f2da7e84a6390a4', IntegrationTestFacebook.access_token)
+ self.facebook = FacebookApi(self.oauth)
+
+ # -----------------------------------------------------------------------
+ def test_authenticate(self):
+ self.facebook.authenticate()
+ IntegrationTestFacebook.access_token = self.oauth.get_access_token()
+
+
+ # -----------------------------------------------------------------------
+ def test_get_user(self):
+ user = self.facebook.get_user()
+ print user
+ assert user
+
+
+ # -----------------------------------------------------------------------
+ def test_get_friends(self):
+ friends = self.facebook.get_friends()
+ print friends
+ assert friends
+
+
+if __name__ == '__main__':
+ unittest.main()
--- /dev/null
+import unittest
+import oauth2
+import httplib
+import simplejson
+httplib.HTTPConnection.debuglevel = 1
+
+class IntegrationTestOAuth2(unittest.TestCase):
+ access_token = None
+
+ # -----------------------------------------------------------------------
+ def setUp(self):
+ self.oauth = oauth2.OAuth2('5916f12942feea4b3247d42a84371112', '19f7538edd96b6870f2da7e84a6390a4', IntegrationTestOAuth2.access_token)
+
+
+ # -----------------------------------------------------------------------
+ def test_authorisation(self):
+ self.oauth.authorise('https://graph.facebook.com/oauth/authorize',
+ 'https://graph.facebook.com/oauth/access_token',
+ {'scope': 'user_about_me,friends_about_me,user_birthday,friends_birthday,user_website,friends_website,user_work_history,friends_work_history'})
+
+ IntegrationTestOAuth2.access_token = self.oauth.get_access_token()
+ assert IntegrationTestOAuth2.access_token
+
+
+ # -----------------------------------------------------------------------
+ def test_request(self):
+ if not IntegrationTestOAuth2.access_token:
+ self.test_authorisation()
+
+ response = self.oauth.request('https://graph.facebook.com/me')
+ data = simplejson.loads(response)
+ if 'error' in data:
+ print response
+ raise Exception(data['error'])
+
+ print response
+ assert 'name' in data and data['name']
+
+
+if __name__ == '__main__':
+ unittest.main()
from org.maemo.hermes.engine.facebook.service import Service
+from org.maemo.hermes.engine.facebook.api import FacebookApi
from org.maemo.hermes.engine.names import canonical
from org.maemo.hermes.engine.friend import Friend
import unittest
def get_identifiers(self):
return [canonical(self.name)]
+class FakeFacebookApi():
+ friends = []
+
+ def get_friends(self):
+ return FakeFacebookApi.friends
+
+class FakeOAuth():
+ def authorise(self, authorise_url, access_token_url, args = None):
+ pass
+
+ def request(self, url, args = None):
+ if url == 'https://graph.facebook.com/me':
+ return '{"id":"1234567","name":"Maemo Hermes"}'
+
+ elif url == 'https://graph.facebook.com/me/friends':
+ return '{"data":[{"id":"1","name":"J Smith","website":"http:\/\/www.example.org"},{"id":"2","name":"P Barnum"}]}'
+
+ else:
+ raise Exception("Unknown URL: %s" % (url))
+
class TestFacebookService(unittest.TestCase):
def setUp(self):
- self.testee = Service("facebook", None)
+ self.testee = Service("facebook", FakeFacebookApi())
def test_that_get_friends_to_create_contacts_for_works(self):
def run_test(expected_length):
- self._fake_server_response([{'uid':'123456','name':'Facebook Junkie', 'birthday_date':'now'}])
+ self._fake_server_response([{'id':'123456','name':'Facebook Junkie', 'birthday':'now'}])
self._run_service([])
friends = self.testee.get_friends_to_create_contacts_for()
assert len(friends) == expected_length
# default is to NOT create contacts
- self.testee = Service("facebook", None)
+ self.testee = Service("facebook", FakeFacebookApi())
run_test(0)
# passing False explicitly
- self.testee = Service("facebook", None, False)
+ self.testee = Service("facebook", FakeFacebookApi(), False)
run_test(0)
# passing True to constructor
- self.testee = Service("facebook", None, True)
+ self.testee = Service("facebook", FakeFacebookApi(), True)
run_test(1)
def test_that_gftccf_returns_friends_with_birth_date(self):
- self.testee = Service("facebook", None, True)
+ self.testee = Service("facebook", FakeFacebookApi(), True)
bday = '1980-10-15'
- props_with_bday = {'uid':'123456','name':'Facebook Junkie', 'birthday_date':bday}
- props_without = {'uid':'123457','name':'Johnny Secret'}
+ props_with_bday = {'id':'123456','name':'Facebook Junkie', 'birthday':bday}
+ props_without = {'id':'123457','name':'Johnny Secret'}
self._fake_server_response([props_with_bday, props_without])
self._run_service([])
def test_that_process_contact_returns_friend_object_for_known_contact(self):
known_url = 'http://www.facebook.com/profile.php?id=123456'
known_contact = FakeContact('Facebook Junkie', [known_url])
- self._fake_server_response([{'uid':'123456','name':'Facebook Junkie'}])
+ self._fake_server_response([{'id':'123456','name':'Facebook Junkie'}])
self.testee.process_friends()
result = self.testee.process_contact(known_contact)
# arrange
self.existing_address = 'http://www.facebook.com/profile.php?id=123456'
self.existing_contact = FakeContact("Facebook Person", [self.existing_address])
- existing_fake = {'uid':'123456','name':'Name Doesnt Match but URL Does'}
+ existing_fake = {'id':'123456','name':'Name Doesnt Match but URL Does'}
self.other_address = 'http://twitter.com/not/correct/site'
self.other_contact = FakeContact("Twitter Person", [self.other_address])
- other_fake = {'uid':'123','name':self.other_contact.get_name()}
+ other_fake = {'id':'123','name':self.other_contact.get_name()}
self.none_contact = FakeContact("No URLson", [])
contact_do_match = FakeContact(name, ["http://www.facebook.com/profile.php?id=123"], 1);
contact_no_match = FakeContact(name, [None], 2)
- data = [{'uid':'123','name':name}]
+ data = [{'id':'123','name':name}]
self._fake_server_response(data)
self._run_service([contact_no_match, contact_do_match])
contact_do_match = FakeContact(name, ["Contact 1"]);
contact_no_match = FakeContact(name, ["Contact 2"])
- data = [{'uid':'123','name':name}]
+ data = [{'id':'123','name':name}]
self._fake_server_response(data)
self._run_service([contact_no_match, contact_do_match])
self.testee.process_contact(contact)
def _fake_server_response(self, data):
- self.testee._get_friends_data = self._get_friends_data
- self._server_response = data
-
- def _get_friends_data(self):
- return self._server_response
+ FakeFacebookApi.friends = data
+
+class TestFacebookAPI(unittest.TestCase):
+
+ def setUp(self):
+ self.oauth = FakeOAuth()
+ self.api = FacebookApi(self.oauth)
+
+ def test_authenticate(self):
+ pass
+
+
+ def test_get_user(self):
+ assert self.api.get_user() == 'Maemo Hermes'
+
+
+ def test_get_friends(self):
+ friends = self.api.get_friends()
+ assert len(friends) == 2
+ assert friends[0]['name'] == 'J Smith'
+ assert friends[1]['id'] == '2'
if __name__ == '__main__':