Add support for creating contacts from "interesting" friends on LinkedIn. Completes...
[hermes] / package / src / org / maemo / hermes / engine / provider_oauth.py
1 from oauth import oauth
2 import gnome.gconf
3 import gobject
4 import gtk
5 import hildon
6 import org.maemo.hermes.engine.provider
7 import time
8 import thread
9 import httplib
10 import re
11 import webbrowser
12
13 class OAuthProvider(org.maemo.hermes.engine.provider.Provider):
14     """Basis for OAuth services for Hermes. Sub-classes of this should
15        install keys to '<id>_...', and implement the following
16        methods:
17        
18            * get_name
19            * get_urls (tuple of request token URL, access token URL & authorize URL)
20            * verify_verifier (returns name of authenticated user)
21            * additional_prefs    [optional]
22            * handle_prefs_button [optional]
23
24        Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
25        Released under the Artistic Licence."""
26
27     GCONF_API_KEY = '/apps/maemo/hermes/%s_key'
28     GCONF_API_SECRET = '/apps/maemo/hermes/%s_secret'
29     GCONF_ACCESS_TOKEN = '/apps/maemo/hermes/%s_access_token'
30     GCONF_USER = '/apps/maemo/hermes/%s_user'
31        
32     # -----------------------------------------------------------------------
33     def __init__(self):
34         """Initialise the provider, and ensure the environment is going to work."""
35
36         self._gc = gnome.gconf.client_get_default()
37         
38         api_key = self._gc.get_string(self.GCONF_API_KEY % (self.get_id()))
39         secret_key = self._gc.get_string(self.GCONF_API_SECRET % (self.get_id()))
40         if api_key is None or secret_key is None:
41             raise Exception('No application keys found for %s. Installation error.' % (self.get_id()))
42
43         self.access_token = self._get_access_token_from_gconf()
44         self.consumer     = oauth.OAuthConsumer(api_key, secret_key)
45         self.sig_method   = oauth.OAuthSignatureMethod_HMAC_SHA1()
46         
47         
48     # -----------------------------------------------------------------------
49     def get_urls(self):
50         """Return a tuple containing request token, access token & authorize URLs."""
51         
52         return (None, None, None)
53
54     
55     # -----------------------------------------------------------------------
56     def get_account_detail(self):
57         """Return the name of the linked LinkedIn account."""
58         
59         return self._gc.get_string(self.GCONF_USER % (self.get_id()))
60     
61     
62     # -----------------------------------------------------------------------
63     def has_preferences(self):
64         """Whether or not this provider has any preferences. If it does not,
65            open_preferences must NOT be called; as the behaviour is undetermined."""
66            
67         return True
68     
69     
70     # -----------------------------------------------------------------------
71     def open_preferences(self, parent):
72         """Open the preferences for this provider as a child of the 'parent' widget."""
73
74         self.main_window = parent
75         dialog = gtk.Dialog(self.get_name(), parent)
76         dialog.add_button(_('Disable'), gtk.RESPONSE_NO)
77         enable = dialog.add_button(_('Enable'), gtk.RESPONSE_YES)
78     
79         button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
80                                hildon.BUTTON_ARRANGEMENT_VERTICAL)
81         self._handle_button(None, button, enable)
82         button.connect('clicked', self._handle_button, button, enable)
83             
84         dialog.vbox.add(button)
85         self.additional_prefs(dialog)
86         dialog.vbox.add(gtk.Label(""))
87         
88         dialog.show_all()
89         result = dialog.run()
90         dialog.hide()
91         
92         if result == gtk.RESPONSE_CANCEL or result == gtk.RESPONSE_DELETE_EVENT:
93             return None
94     
95         self.handle_prefs_response(result)
96         return result == gtk.RESPONSE_YES
97
98
99     # -----------------------------------------------------------------------
100     def additional_prefs(self, dialog):
101         """Override to add additional controls to the authentication dialogue."""
102         
103         dialog.vbox.add(gtk.Label(""))
104         dialog.vbox.add(gtk.Label(""))
105
106
107     # -----------------------------------------------------------------------
108     def handle_prefs_response(self, result):
109         """Override to handle the response from a dialogue button."""
110         
111         None
112
113
114     # -----------------------------------------------------------------------
115     def _handle_button(self, e, button, enable):
116         """Ensure the button state is correct."""
117         
118         authenticated = self._get_access_token_from_gconf() is not None
119         if e is not None:
120             if authenticated:
121                 self._remove_access_token_from_gconf()
122             else:
123                 self.authenticate(lambda: None, self.block_for_auth)
124         
125             authenticated = self._get_access_token_from_gconf() is not None
126         
127         button.set_title(authenticated and _("Clear authorisation") or _("Authorise"))
128         enable.set_sensitive(authenticated)
129
130         
131     # -----------------------------------------------------------------------
132     def block_for_auth(self, url):
133         """Part of the GUI callback API."""
134
135         webbrowser.open(url)
136         time.sleep(3)
137         note = gtk.Dialog(_('Service authorisation'), self.main_window)
138         note.add_button(_("Validate"), gtk.RESPONSE_OK)
139         input = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
140         input.set_property('is-focus', False)
141         input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_NUMERIC)
142         note.set_title(_("Verification code from web browser"))
143         note.vbox.add(input)
144
145         note.show_all()
146         result = note.run()
147         note.hide()
148         if result == gtk.RESPONSE_OK:
149             return input.get_text()
150         else:
151             return None
152
153
154     # -----------------------------------------------------------------------
155     def authenticate(self, need_auth, block_for_auth):
156         need_auth()
157         token = self._get_request_token()
158         url = self._get_authorize_url(token)
159         verifier = block_for_auth(url)
160         self.access_token = self._get_access_token(token, verifier)
161         name = self.verify_verifier(self.access_token)
162         self._gc.set_string(self.GCONF_USER % (self.get_id()), name)
163
164
165     # -----------------------------------------------------------------------
166     def _store_access_token_in_gconf(self, token_str):
167         if "oauth_problem" in token_str:
168             raise Exception("Authorization failure - access token reported OAuth problem")
169         
170         self._gc.set_string(self.GCONF_ACCESS_TOKEN % (self.get_id()), token_str)
171
172         
173     # -----------------------------------------------------------------------
174     def _get_access_token_from_gconf(self):
175         """Returns an oauth.OAuthToken, or None if the gconf value is empty"""
176         
177         token_str = self._gc.get_string(self.GCONF_ACCESS_TOKEN % (self.get_id()))
178         if not token_str or len(token_str) < 8:
179             return None
180         try:
181             return oauth.OAuthToken.from_string(token_str)
182         except KeyError, e:
183             print "Invalid: ", token_str
184             return None
185
186
187     # -----------------------------------------------------------------------
188     def _remove_access_token_from_gconf(self):
189         """Remove the oauth.OAuthToken, if any."""
190         
191         self._gc.unset(self.GCONF_ACCESS_TOKEN % (self.get_id()))
192         self._gc.unset(self.GCONF_USER % (self.get_id()))
193
194
195     # -----------------------------------------------------------------------
196     def _get_request_token(self):
197         """Get a request token from OAuth provider."""
198         
199         url = self.get_urls()[0]
200         hostname = self._get_hostname_from_url(url)
201
202         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, callback="oob", http_url=url)
203         oauth_request.sign_request(self.sig_method, self.consumer, None)
204
205         connection = httplib.HTTPSConnection(hostname)
206         connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
207         response = connection.getresponse().read()
208         
209         try:
210             token = oauth.OAuthToken.from_string(response)
211         except Exception, e:
212             import traceback
213             traceback.print_exc()
214             print response
215             raise Exception("Authorization failure - failed to get request token")
216         return token
217
218     
219     # -----------------------------------------------------------------------
220     def _get_authorize_url(self, token):
221         """The URL that the user should browse to, in order to authorize the 
222            application's request to access data"""
223         
224         oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.get_urls()[2])
225         return oauth_request.to_url()
226
227
228     # -----------------------------------------------------------------------
229     def _get_access_token(self, token, verifier):
230         """If the verifier (which was displayed in the browser window) is 
231            valid, then an access token is returned which should be used to 
232            access data on the service."""
233         
234         url = self.get_urls()[1]
235         hostname = self._get_hostname_from_url(url)
236         
237         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, verifier=verifier, http_url=url)
238         oauth_request.sign_request(self.sig_method, self.consumer, token)
239
240         connection = httplib.HTTPSConnection(hostname)
241         connection.request(oauth_request.http_method, url, headers=oauth_request.to_header()) 
242         response = connection.getresponse()
243         token_str = response.read()
244         if 'oauth_problem' in token_str:
245             raise Exception("Authorization failure - failed to get access token (" + token_str + ")")
246         self._store_access_token_in_gconf(token_str)
247         return oauth.OAuthToken.from_string(token_str)
248     
249
250     # -----------------------------------------------------------------------
251     def make_api_request(self, url):
252         hostname = self._get_hostname_from_url(url)
253
254         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
255         oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
256         connection = httplib.HTTPSConnection(hostname)
257         try:
258             connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
259             xml = connection.getresponse().read()
260             return xml
261         except:
262             raise Exception("Failed to contact LinkedIn at " + url)
263
264
265     # -----------------------------------------------------------------------
266     def _get_hostname_from_url(self, url):
267         """Extract the hostname from a URL."""
268         return re.sub(r'^\w+://(.+?)[/:].*$', r'\1', url)
269