Implement OAuth for Twitter, and share common infrastructure for all
authorAndrew Flegg <andrew@bleb.org>
Sat, 16 Oct 2010 19:40:51 +0000 (20:40 +0100)
committerAndrew Flegg <andrew@bleb.org>
Sat, 16 Oct 2010 19:40:51 +0000 (20:40 +0100)
OAuth providers. Fixes MB#11275.

package/debian/hermes.postinst
package/src/org/maemo/hermes/engine/facebook/service.py
package/src/org/maemo/hermes/engine/linkedin/api.py
package/src/org/maemo/hermes/engine/linkedin/provider.py
package/src/org/maemo/hermes/engine/provider_oauth.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/syncjob.py [deleted file]
package/src/org/maemo/hermes/engine/twitter/api.py
package/src/org/maemo/hermes/engine/twitter/provider.py

index 03d2312..5208d64 100644 (file)
@@ -7,10 +7,8 @@ gconftool-2 -s /apps/maemo/hermes/gravatar_email maemohermes@wendt.se --type str
 gconftool-2 -s /apps/maemo/hermes/gravatar_key b14ec179822b --type string
 gconftool-2 -s /apps/maemo/hermes/linkedin_key 1et4G-VtmtqNfY7gF8PHtxMOf0KNWl9ericlTEtdKJeoA4ubk4wEQwf8lSL8AnYE --type string
 gconftool-2 -s /apps/maemo/hermes/linkedin_secret uk--OtmWcxER-Yh6Py5p0VeLPNlDJSMaXj1xfHILoFzrK7fM9eepNo5RbwGdkRo_ --type string
-
-# Version 0.2.1 changed type of /apps/maemo/hermes/uid
-OLD_UID=`gconftool-2 -g /apps/maemo/hermes/uid`
-[ -z "$OLD_UID" ] || gconftool-2 -s /apps/maemo/hermes/uid "$OLD_UID" --type string
+gconftool-2 -s /apps/maemo/hermes/twitter_key pBpcz09kp71jnEdZWqjQ --type string
+gconftool-2 -s /apps/maemo/hermes/twitter_secret wBwyJsQIZFsKMhCA0S9C0b5KmWSz07T3z0KWpKs --type string
 
 # Hacky fix for NB#136012
 gconftool-2 -s /desktop/gnome/url-handlers/http/command 'dbus-send --type=method_call --dest=com.nokia.osso_browser /com/nokia/osso_browser com.nokia.osso_browser.load_url string:"%s"' --type string 
index aa912c5..91dfe33 100644 (file)
@@ -76,6 +76,7 @@ class Service(org.maemo.hermes.engine.service.Service):
         
         def if_defined(data, key, callback):
             if key in data and data[key]:
+                print key, data[key]
                 callback(data[key])
         
         friends_data = self._get_friends_data()
index 101a526..ebb0c60 100644 (file)
@@ -12,50 +12,31 @@ class LinkedInApi():
        Copyright (c) Fredrik Wendt <fredrik@wendt.se> 2010.
        Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
        Released under the Artistic Licence."""
-       
-    GCONF_API_KEY = '/apps/maemo/hermes/linkedin_key'
-    GCONF_API_SECRET = '/apps/maemo/hermes/linkedin_secret'
-    GCONF_ACCESS_TOKEN = '/apps/maemo/hermes/linkedin_access_token'
-    GCONF_USER = '/apps/maemo/hermes/linkedin_user'
     
-    LI_SERVER = "api.linkedin.com"
     LI_API_URL = "https://api.linkedin.com"
     LI_CONN_API_URL = LI_API_URL + "/v1/people/~/connections:(id,first-name,last-name,picture-url,site-standard-profile-request:(url),date-of-birth,main-address,location:(country:(code)),phone-numbers,member-url-resources)"
     LI_PROFILE_API_URL = LI_API_URL + "/v1/people/~"
-
-    REQUEST_TOKEN_URL = LI_API_URL + "/uas/oauth/requestToken"
-    AUTHORIZE_URL = LI_API_URL + "/uas/oauth/authorize"
-    ACCESS_TOKEN_URL = LI_API_URL + "/uas/oauth/accessToken"
-
-
+    
+    
     # -----------------------------------------------------------------------
-    def __init__(self, gconf=None):
-        """Initialize the LinkedIn service, finding LinkedIn API keys in gconf and
-           having a gui_callback available."""
-        
-        if gconf: self._gc = gconf
-        else: self._gc = gnome.gconf.client_get_default()
-        
-        api_key = self._gc.get_string(LinkedInApi.GCONF_API_KEY)
-        secret_key = self._gc.get_string(LinkedInApi.GCONF_API_SECRET)
-        self.api_key = api_key
-
-        if api_key is None or secret_key is None:
-            raise Exception('No LinkedIn application keys found. Installation error.')
-
-        self.access_token = self.get_access_token_from_gconf()
-
-        self.consumer = oauth.OAuthConsumer(api_key, secret_key)
-        self.sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
-
+    def __init__(self, consumer, api_request):
+        """Instantiate with an OAuth consumer"""
+        self.consumer = consumer
+        self._make_api_request = api_request
+    
 
     # -----------------------------------------------------------------------
-    def authenticate(self, need_auth, block_for_auth):
-        need_auth()
-        token = self._get_request_token()
-        url = self._get_authorize_url(token)
-        verifier = block_for_auth(url)
-        self._verify_verifier(token, verifier)
+    def verify_verifier(self, access_token):
+        try:
+            xml = self._make_api_request(self.LI_PROFILE_API_URL)
+            dom = parseString(xml)
+            friends = self._parse_dom(dom)
+            return friends[0].get_name()
+        except Exception, e:
+            import traceback
+            traceback.print_exc()
+            raise Exception("LinkedIn authorization failed, try again")
+
     
     
     # -----------------------------------------------------------------------
@@ -80,19 +61,6 @@ class LinkedInApi():
         connection.request(oauth_request.http_method, url, headers=headers)
         data = connection.getresponse().read()
         return data
-    
-
-    # -----------------------------------------------------------------------
-    def _make_api_request(self, url):
-        oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
-        oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
-        connection = httplib.HTTPSConnection(self.LI_SERVER)
-        try:
-            connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
-            xml = connection.getresponse().read()
-            return xml
-        except:
-            raise Exception("Failed to contact LinkedIn at " + url)
 
 
     # -----------------------------------------------------------------------
@@ -118,23 +86,25 @@ class LinkedInApi():
             country = extract(get_first_tag(node, 'country'), 'code')
             tag = get_first_tag(node, 'phone-numbers')
             numbers = []
-            for element in tag.childNodes:
-                if element.nodeName != 'phone-number':
-                    continue
-                 
-                phone_type = extract(element, 'phone-type')
-                device = phone_type == 'mobile' and phone_type or None
-                type = phone_type in set(['home', 'work']) and phone_type or None
-                
-                number = PhoneNumber(extract(element, 'phone-number'), device = device, type = type, country = country)
-                numbers.append(number)
+            if tag:
+                for element in tag.childNodes:
+                    if element.nodeName != 'phone-number':
+                        continue
+                     
+                    phone_type = extract(element, 'phone-type')
+                    device = phone_type == 'mobile' and phone_type or None
+                    type = phone_type in set(['home', 'work']) and phone_type or None
+                    
+                    number = PhoneNumber(extract(element, 'phone-number'), device = device, type = type, country = country)
+                    numbers.append(number)
             return numbers
         
         def extract_urls(node):
             tag = get_first_tag(node, 'member-url-resources')
             urls = []
-            for element in tag.getElementsByTagName('url'):
-                urls.append(element.firstChild.nodeValue.replace("&amp;", "&"))
+            if tag:
+                for element in tag.getElementsByTagName('url'):
+                    urls.append(element.firstChild.nodeValue.replace("&amp;", "&"))
             return urls
         
         def extract_birthday(node):
@@ -153,7 +123,7 @@ class LinkedInApi():
         
         # look for errors
         errors = dom.getElementsByTagName('error')
-        if (len(errors) > 0):
+        if (errors and len(errors) > 0):
             details = ""
             try:
                 details = " (" + extract(errors[0], "message") + ")"
@@ -189,92 +159,3 @@ class LinkedInApi():
             friends.append(friend)
 
         return friends
-
-    # -----------------------------------------------------------------------
-    def _get_request_token(self):
-        """Get a request token from LinkedIn"""
-        
-        oauth_consumer_key = self.api_key
-        oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, callback="oob", http_url=self.REQUEST_TOKEN_URL)
-        oauth_request.sign_request(self.sig_method, self.consumer, None)
-
-        connection = httplib.HTTPSConnection(self.LI_SERVER)
-        connection.request(oauth_request.http_method, self.REQUEST_TOKEN_URL, headers=oauth_request.to_header())
-        response = connection.getresponse().read()
-        
-        try:
-            token = oauth.OAuthToken.from_string(response)
-        except Exception, e:
-            import traceback
-            traceback.print_exc()
-            print response
-            raise Exception("Authorization failure - failed to get request token")
-        return token
-
-    
-    # -----------------------------------------------------------------------
-    def _get_authorize_url(self, token):
-        """The URL that the user should browse to, in order to authorize the 
-           application's request to access data"""
-        
-        oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.AUTHORIZE_URL)
-        return oauth_request.to_url()
-
-
-    # -----------------------------------------------------------------------
-    def _get_access_token(self, token, verifier):
-        """If the verifier (which was displayed in the browser window) is 
-           valid, then an access token is returned which should be used to 
-           access data on the service."""
-        
-        oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, verifier=verifier, http_url=self.ACCESS_TOKEN_URL)
-        oauth_request.sign_request(self.sig_method, self.consumer, token)
-
-        connection = httplib.HTTPSConnection(self.LI_SERVER)
-        connection.request(oauth_request.http_method, self.ACCESS_TOKEN_URL, headers=oauth_request.to_header()) 
-        response = connection.getresponse()
-        token_str = response.read()
-        if 'oauth_problem' in token_str:
-            raise Exception("Authorization failure - failed to get access token (" + token_str + ")")
-        self._store_access_token_in_gconf(token_str)
-        return oauth.OAuthToken.from_string(token_str)
-
-
-    # -----------------------------------------------------------------------
-    def _verify_verifier(self, request_token, verifier):
-        try:
-            self.access_token = self._get_access_token(request_token, verifier)
-            xml = self._make_api_request(self.LI_PROFILE_API_URL)
-            dom = parseString(xml)
-            friends = self._parse_dom(dom)
-            self._gc.set_string(LinkedInApi.GCONF_USER, friends[0].get_name())
-        except Exception, e:
-            import traceback
-            traceback.print_exc()
-            raise Exception("LinkedIn authorization failed, try again")
-
-
-    # -----------------------------------------------------------------------
-    def _store_access_token_in_gconf(self, token_str):
-        if "oauth_problem" in token_str:
-            raise Exception("Authorization failure - access token reported OAuth problem")
-        
-        self._gc.set_string(LinkedInApi.GCONF_ACCESS_TOKEN, token_str)
-
-        
-    # -----------------------------------------------------------------------
-    def get_access_token_from_gconf(self):
-        """Returns an oauth.OAuthToken, or None if the gconf value is empty"""
-        
-        token_str = self._gc.get_string(LinkedInApi.GCONF_ACCESS_TOKEN)
-        if not token_str:
-            return None
-        return oauth.OAuthToken.from_string(token_str)
-
-
-    # -----------------------------------------------------------------------
-    def remove_access_token_from_gconf(self):
-        """Remove the oauth.OAuthToken, if any."""
-        
-        self._gc.unset(LinkedInApi.GCONF_ACCESS_TOKEN)
-        self._gc.unset(LinkedInApi.GCONF_USER)
index 9edb025..3a915a2 100644 (file)
@@ -2,22 +2,27 @@ import gnome.gconf
 import gobject, gtk, hildon
 import time, thread
 import webbrowser
-import org.maemo.hermes.engine.provider
+from org.maemo.hermes.engine.provider_oauth import OAuthProvider
 import org.maemo.hermes.engine.linkedin.service
 from org.maemo.hermes.engine.linkedin.api import LinkedInApi
 
-class Provider(org.maemo.hermes.engine.provider.Provider):
+class Provider(OAuthProvider):
     """LinkedIn provider for Hermes. 
 
        Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
        Released under the Artistic Licence."""
-       
+
+    LI_API_URL = "https://api.linkedin.com"
+
+    REQUEST_TOKEN_URL = LI_API_URL + "/uas/oauth/requestToken"
+    AUTHORIZE_URL = LI_API_URL + "/uas/oauth/authorize"
+    ACCESS_TOKEN_URL = LI_API_URL + "/uas/oauth/accessToken"
+
+
     # -----------------------------------------------------------------------
     def __init__(self):
-        """Initialise the provider, and ensure the environment is going to work."""
-
-        self._gc = gnome.gconf.client_get_default()
-        self._api = LinkedInApi(gconf = self._gc) 
+        OAuthProvider.__init__(self)
+        self._api = LinkedInApi(self.consumer, self.make_api_request)
 
 
     # -----------------------------------------------------------------------
@@ -29,85 +34,10 @@ class Provider(org.maemo.hermes.engine.provider.Provider):
     
     
     # -----------------------------------------------------------------------
-    def get_account_detail(self):
-        """Return the name of the linked LinkedIn account."""
-        
-        return self._gc.get_string(LinkedInApi.GCONF_USER)
-    
-    
-    # -----------------------------------------------------------------------
-    def has_preferences(self):
-        """Whether or not this provider has any preferences. If it does not,
-           open_preferences must NOT be called; as the behaviour is undetermined."""
+    def get_urls(self):
+        """Return the various URLs needed for OAuth."""
            
-        return True
-    
-    
-    # -----------------------------------------------------------------------
-    def open_preferences(self, parent):
-        """Open the preferences for this provider as a child of the 'parent' widget."""
-
-        self.main_window = parent
-        dialog = gtk.Dialog(self.get_name(), parent)
-        dialog.add_button(_('Disable'), gtk.RESPONSE_NO)
-        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(gtk.Label(""))
-        dialog.vbox.add(button)
-        dialog.vbox.add(gtk.Label(""))
-        
-        dialog.show_all()
-        result = dialog.run()
-        dialog.hide()
-        if result == gtk.RESPONSE_CANCEL or result == gtk.RESPONSE_DELETE_EVENT:
-            return None
-    
-        return result == gtk.RESPONSE_YES
-
-
-    # -----------------------------------------------------------------------
-    def _handle_button(self, e, button, enable):
-        """Ensure the button state is correct."""
-        
-        authenticated = self._api.get_access_token_from_gconf() is not None
-        if e is not None:
-            if authenticated:
-                self._api.remove_access_token_from_gconf()
-            else:
-                self._api.authenticate(lambda: None, self.block_for_auth)
-        
-            authenticated = self._api.get_access_token_from_gconf() is not None
-        
-        button.set_title(authenticated and _("Clear authorisation") or _("Authorise"))
-        enable.set_sensitive(authenticated)
-
-        
-    # -----------------------------------------------------------------------
-    def block_for_auth(self, url):
-        """Part of the GUI callback API."""
-
-        webbrowser.open(url)
-        time.sleep(3)
-        note = gtk.Dialog(_('Service authorisation'), self.main_window)
-        note.add_button(_("Validate"), gtk.RESPONSE_OK)
-        input = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
-        input.set_property('is-focus', False)
-        input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_NUMERIC)
-        note.set_title(_("Verification code from web browser"))
-        note.vbox.add(input)
-
-        note.show_all()
-        result = note.run()
-        note.hide()
-        if result == gtk.RESPONSE_OK:
-            return input.get_text()
-        else:
-            return None
+        return (self.REQUEST_TOKEN_URL, self.ACCESS_TOKEN_URL, self.AUTHORIZE_URL)
 
     
     # -----------------------------------------------------------------------
@@ -115,3 +45,8 @@ class Provider(org.maemo.hermes.engine.provider.Provider):
         """Return the service backend."""
            
         return org.maemo.hermes.engine.linkedin.service.Service(self.get_id(), self._api)
+
+
+    # -----------------------------------------------------------------------
+    def verify_verifier(self, access_token):
+        return self._api.verify_verifier(access_token)
diff --git a/package/src/org/maemo/hermes/engine/provider_oauth.py b/package/src/org/maemo/hermes/engine/provider_oauth.py
new file mode 100644 (file)
index 0000000..95b8684
--- /dev/null
@@ -0,0 +1,266 @@
+from oauth import oauth
+import gnome.gconf
+import gobject
+import gtk
+import hildon
+import org.maemo.hermes.engine.provider
+import time
+import thread
+import httplib
+import re
+import webbrowser
+
+class OAuthProvider(org.maemo.hermes.engine.provider.Provider):
+    """Basis for OAuth services for Hermes. Sub-classes of this should
+       install keys to '<id>_...', and implement the following
+       methods:
+       
+           * get_name
+           * get_urls (tuple of request token URL, access token URL & authorize URL)
+           * verify_verifier (returns name of authenticated user)
+           * additional_prefs    [optional]
+           * handle_prefs_button [optional]
+
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+       Released under the Artistic Licence."""
+
+    GCONF_API_KEY = '/apps/maemo/hermes/%s_key'
+    GCONF_API_SECRET = '/apps/maemo/hermes/%s_secret'
+    GCONF_ACCESS_TOKEN = '/apps/maemo/hermes/%s_access_token'
+    GCONF_USER = '/apps/maemo/hermes/%s_user'
+       
+    # -----------------------------------------------------------------------
+    def __init__(self):
+        """Initialise the provider, and ensure the environment is going to work."""
+
+        self._gc = gnome.gconf.client_get_default()
+        
+        api_key = self._gc.get_string(self.GCONF_API_KEY % (self.get_id()))
+        secret_key = self._gc.get_string(self.GCONF_API_SECRET % (self.get_id()))
+        if api_key is None or secret_key is None:
+            raise Exception('No application keys found for %s. Installation error.' % (self.get_id()))
+
+        self.access_token = self._get_access_token_from_gconf()
+        self.consumer     = oauth.OAuthConsumer(api_key, secret_key)
+        self.sig_method   = oauth.OAuthSignatureMethod_HMAC_SHA1()
+        
+        
+    # -----------------------------------------------------------------------
+    def get_urls(self):
+        """Return a tuple containing request token, access token & authorize URLs."""
+        
+        return (None, None, None)
+
+    
+    # -----------------------------------------------------------------------
+    def get_account_detail(self):
+        """Return the name of the linked LinkedIn account."""
+        
+        return self._gc.get_string(self.GCONF_USER % (self.get_id()))
+    
+    
+    # -----------------------------------------------------------------------
+    def has_preferences(self):
+        """Whether or not this provider has any preferences. If it does not,
+           open_preferences must NOT be called; as the behaviour is undetermined."""
+           
+        return True
+    
+    
+    # -----------------------------------------------------------------------
+    def open_preferences(self, parent):
+        """Open the preferences for this provider as a child of the 'parent' widget."""
+
+        self.main_window = parent
+        dialog = gtk.Dialog(self.get_name(), parent)
+        dialog.add_button(_('Disable'), gtk.RESPONSE_NO)
+        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)
+        self.additional_prefs(dialog)
+        dialog.vbox.add(gtk.Label(""))
+        
+        dialog.show_all()
+        result = dialog.run()
+        dialog.hide()
+        
+        self.handle_prefs_response(result)
+        if result == gtk.RESPONSE_CANCEL or result == gtk.RESPONSE_DELETE_EVENT:
+            return None
+    
+        return result == gtk.RESPONSE_YES
+
+
+    # -----------------------------------------------------------------------
+    def additional_prefs(self, dialog):
+        """Override to add additional controls to the authentication dialogue."""
+        
+        dialog.vbox.add(gtk.Label(""))
+        dialog.vbox.add(gtk.Label(""))
+
+
+    # -----------------------------------------------------------------------
+    def handle_prefs_response(self, result):
+        """Override to handle the response from a dialogue button."""
+        
+        None
+
+
+    # -----------------------------------------------------------------------
+    def _handle_button(self, e, button, enable):
+        """Ensure the button state is correct."""
+        
+        authenticated = self._get_access_token_from_gconf() is not None
+        if e is not None:
+            if authenticated:
+                self._remove_access_token_from_gconf()
+            else:
+                self.authenticate(lambda: None, self.block_for_auth)
+        
+            authenticated = self._get_access_token_from_gconf() is not None
+        
+        button.set_title(authenticated and _("Clear authorisation") or _("Authorise"))
+        enable.set_sensitive(authenticated)
+
+        
+    # -----------------------------------------------------------------------
+    def block_for_auth(self, url):
+        """Part of the GUI callback API."""
+
+        webbrowser.open(url)
+        time.sleep(3)
+        note = gtk.Dialog(_('Service authorisation'), self.main_window)
+        note.add_button(_("Validate"), gtk.RESPONSE_OK)
+        input = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+        input.set_property('is-focus', False)
+        input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_NUMERIC)
+        note.set_title(_("Verification code from web browser"))
+        note.vbox.add(input)
+
+        note.show_all()
+        result = note.run()
+        note.hide()
+        if result == gtk.RESPONSE_OK:
+            return input.get_text()
+        else:
+            return None
+
+
+    # -----------------------------------------------------------------------
+    def authenticate(self, need_auth, block_for_auth):
+        need_auth()
+        token = self._get_request_token()
+        url = self._get_authorize_url(token)
+        verifier = block_for_auth(url)
+        self.access_token = self._get_access_token(token, verifier)
+        name = self.verify_verifier(self.access_token)
+        self._gc.set_string(self.GCONF_USER % (self.get_id()), name)
+
+
+    # -----------------------------------------------------------------------
+    def _store_access_token_in_gconf(self, token_str):
+        if "oauth_problem" in token_str:
+            raise Exception("Authorization failure - access token reported OAuth problem")
+        
+        self._gc.set_string(self.GCONF_ACCESS_TOKEN % (self.get_id()), token_str)
+
+        
+    # -----------------------------------------------------------------------
+    def _get_access_token_from_gconf(self):
+        """Returns an oauth.OAuthToken, or None if the gconf value is empty"""
+        
+        token_str = self._gc.get_string(self.GCONF_ACCESS_TOKEN % (self.get_id()))
+        print token_str
+        if not token_str or len(token_str) < 8:
+            return None
+        return oauth.OAuthToken.from_string(token_str)
+
+
+    # -----------------------------------------------------------------------
+    def _remove_access_token_from_gconf(self):
+        """Remove the oauth.OAuthToken, if any."""
+        
+        self._gc.unset(self.GCONF_ACCESS_TOKEN % (self.get_id()))
+        self._gc.unset(self.GCONF_USER % (self.get_id()))
+
+
+    # -----------------------------------------------------------------------
+    def _get_request_token(self):
+        """Get a request token from OAuth provider."""
+        
+        url = self.get_urls()[0]
+        hostname = self._get_hostname_from_url(url)
+
+        oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, callback="oob", http_url=url)
+        oauth_request.sign_request(self.sig_method, self.consumer, None)
+
+        connection = httplib.HTTPSConnection(hostname)
+        connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
+        response = connection.getresponse().read()
+        
+        try:
+            token = oauth.OAuthToken.from_string(response)
+        except Exception, e:
+            import traceback
+            traceback.print_exc()
+            print response
+            raise Exception("Authorization failure - failed to get request token")
+        return token
+
+    
+    # -----------------------------------------------------------------------
+    def _get_authorize_url(self, token):
+        """The URL that the user should browse to, in order to authorize the 
+           application's request to access data"""
+        
+        oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.get_urls()[2])
+        return oauth_request.to_url()
+
+
+    # -----------------------------------------------------------------------
+    def _get_access_token(self, token, verifier):
+        """If the verifier (which was displayed in the browser window) is 
+           valid, then an access token is returned which should be used to 
+           access data on the service."""
+        
+        url = self.get_urls()[1]
+        hostname = self._get_hostname_from_url(url)
+        
+        oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, verifier=verifier, http_url=url)
+        oauth_request.sign_request(self.sig_method, self.consumer, token)
+
+        connection = httplib.HTTPSConnection(hostname)
+        connection.request(oauth_request.http_method, url, headers=oauth_request.to_header()) 
+        response = connection.getresponse()
+        token_str = response.read()
+        if 'oauth_problem' in token_str:
+            raise Exception("Authorization failure - failed to get access token (" + token_str + ")")
+        self._store_access_token_in_gconf(token_str)
+        return oauth.OAuthToken.from_string(token_str)
+    
+
+    # -----------------------------------------------------------------------
+    def make_api_request(self, url):
+        hostname = self._get_hostname_from_url(url)
+
+        oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
+        oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
+        connection = httplib.HTTPSConnection(hostname)
+        try:
+            connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
+            xml = connection.getresponse().read()
+            return xml
+        except:
+            raise Exception("Failed to contact LinkedIn at " + url)
+
+
+    # -----------------------------------------------------------------------
+    def _get_hostname_from_url(self, url):
+        """Extract the hostname from a URL."""
+        return re.sub(r'^\w+://(.+?)[/:].*$', r'\1', url)
+     
\ No newline at end of file
diff --git a/package/src/org/maemo/hermes/engine/syncjob.py b/package/src/org/maemo/hermes/engine/syncjob.py
deleted file mode 100644 (file)
index f35ebe1..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/env python
-
-class SyncJob():
-
-    def __init__(self, services, original_contacts, progress_callback):
-        self._services = services
-        self._original_contacts = original_contacts
-    
-        
-    def run(self):
-        self._pre_process_contacts()
-        self._process_friends()
-        self._process_contacts()
-    
-    
-    def create_contact_for_friend(self, friend):
-        """Creates a contact for the friend:
-           FIXME: if we do it NOW, then that contact can be re-used and mapped to other unmatched friends
-           FIXME: should this really take place here ..."""
-        
-        pass
-    
-    
-    def register_match(self, contact, friend):
-        """Makes sure that when save is clicked,""" 
-    
-        
-    def get_unmatched_friends(self):
-        pass
-    
-    
-    def get_updated_contacts(self):
-        pass
-    
-    
-    def get_matched_contacts(self):
-        return self._original_contacts    
-    
-    
-    def _pre_process_contacts(self):
-        for contact in self._original_contacts:
-            for service in self._services:
-                service.pre_process_contact(contact)
-    
-    
-    def _process_friends(self):
-        for service in self._services:
-            service.process_friends()
-    
-    
-    def _process_contacts(self):
-        for contact in self._original_contacts:
-            for service in self._services:
-                service.process_contact(contact)
index 44aca61..b20032c 100644 (file)
@@ -13,20 +13,19 @@ class TwitterApi():
        
        
     # -----------------------------------------------------------------------
-    def __init__(self, username, password):
-        self._username = username
-        self._password = password
+    def __init__(self, make_api_request):
+        self._make_api_request = make_api_request
 
 
     # -----------------------------------------------------------------------
     def get_friends(self):
         '''Return the full list of people being followed by 'username'.'''
 
-        url = 'https://twitter.com/statuses/friends.json'
+        url = 'https://twitter.com/statuses/friends.json?cursor=%d'
         cursor = -1
         users = []
         while True:
-            json = self._FetchUrl(url, parameters = { 'cursor': cursor})
+            json = self._make_api_request(url % (cursor))
             data = simplejson.loads(json)
             if 'error' in data:
                 raise Exception(data['error'])
@@ -39,89 +38,15 @@ class TwitterApi():
                 break
 
         return users
-
-
-    # -----------------------------------------------------------------------
-    def _FetchUrl(self,
-                  url,
-                  parameters=None):
-        '''Fetch a URL, optionally caching for a specified time.
-
-        Args:
-          url:
-            The URL to retrieve
-          parameters:
-            A dict whose key/value pairs should encoded and added
-            to the query string. [optional]
-
-        Returns:
-          A string containing the body of the response.
-        '''
-
-        # Build the extra parameters dict
-        extra_params = {}
-        if parameters:
-            extra_params.update(parameters)
-
-        # Add key/value parameters to the query string of the url
-        url = self._BuildUrl(url, extra_params=extra_params)
-
-        # Get a url opener that can handle basic auth
-        basic_auth = base64.encodestring('%s:%s' % (self._username, self._password))[:-1]
-
-        handler = urllib2.HTTPBasicAuthHandler()
-        (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
-        handler.add_password('Twitter API', netloc, self._username, self._password)
-        opener = urllib2.build_opener(handler)
-        opener.addheaders = {'Authorization': 'Basic %s' % basic_auth}.items()
     
-        url_data = opener.open(url, None).read()
-        opener.close()
-        return url_data
-
-
-    # -----------------------------------------------------------------------
-    def _BuildUrl(self, url, path_elements=None, extra_params=None):
-        # Break url into consituent parts
-        (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
-
-        # Add any additional path elements to the path
-        if path_elements:
-            # Filter out the path elements that have a value of None
-            p = [i for i in path_elements if i]
-            if not path.endswith('/'):
-                path += '/'
-            path += '/'.join(p)
-
-        # Add any additional query parameters to the query string
-        if extra_params and len(extra_params) > 0:
-            extra_query = self._EncodeParameters(extra_params)
-            # Add it to the existing query
-            if query:
-                query += '&' + extra_query
-            else:
-                query = extra_query
-
-        # Return the rebuilt URL
-        return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
-
-
-
+    
     # -----------------------------------------------------------------------
-    def _EncodeParameters(self, parameters):
-        '''Return a string in key=value&key=value form
-
-        Values of None are not included in the output string.
-
-        Args:
-          parameters:
-            A dict of (key, value) tuples, where value is encoded as
-            specified by self._encoding
-        Returns:
-          A URL-encoded string in "key=value&key=value" form
-        '''
-        if parameters is None:
-            return None
-        else:
-            return urllib.urlencode(dict([(k, unicode(v).encode('utf-8')) for k, v in parameters.items() if v is not None]))
-
+    def get_user(self):
+        """Return the name of the authenticated user."""
+        
+        url = 'https://api.twitter.com/1/account/verify_credentials.json'
+        json = self._make_api_request(url)
+        user = simplejson.loads(json)
+#        if 'name' in user and user['name']:
+#            return user['name']
+        return user['screen_name']
index 66c01aa..d91d354 100644 (file)
@@ -1,10 +1,10 @@
 import gnome.gconf
 import gtk, hildon
-import org.maemo.hermes.engine.provider
 import org.maemo.hermes.engine.twitter.service
+from org.maemo.hermes.engine.provider_oauth import OAuthProvider
 from org.maemo.hermes.engine.twitter.api import TwitterApi
 
-class Provider(org.maemo.hermes.engine.provider.Provider):
+class Provider(OAuthProvider):
     """Twitter provider for Hermes. 
 
        Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
@@ -13,7 +13,8 @@ class Provider(org.maemo.hermes.engine.provider.Provider):
 
     # -----------------------------------------------------------------------
     def __init__(self):
-        self._gconf  = gnome.gconf.client_get_default()
+        OAuthProvider.__init__(self)
+        self._api = TwitterApi(self.make_api_request)
 
 
     # -----------------------------------------------------------------------
@@ -22,75 +23,16 @@ class Provider(org.maemo.hermes.engine.provider.Provider):
            all-alphabetic version of this name is expected to be provided."""
            
         return 'Twitter'
-    
-    
-    # -----------------------------------------------------------------------
-    def has_preferences(self):
-        """Whether or not this provider has any preferences. If it does not,
-           open_preferences must NOT be called; as the behaviour is undetermined."""
-           
-        return True
-    
-    
-    # -----------------------------------------------------------------------
-    def open_preferences(self, parent):
-        """Show the username/password dialogue."""
-           
-        dialog = gtk.Dialog(self.get_name(), parent)
-        dialog.add_button(_('Disable'), gtk.RESPONSE_NO)
-        enable = dialog.add_button(_('Enable'), gtk.RESPONSE_YES)
-
-        # -- Username...
-        #
-        hbox = gtk.HBox()
-        hbox.pack_start(gtk.Label(_("Username")))
-        
-        username = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
-        username.set_property('is-focus', True)
-        username.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL)
-        username.set_text(self._gconf.get_string("/apps/maemo/hermes/twitter_user") or '')
-
-        hbox.pack_start(username)
-        dialog.vbox.add(hbox)
-        
-        # -- Password...
-        #
-        hbox = gtk.HBox()
-        hbox.pack_start(gtk.Label(_("Password")))
-        
-        password = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
-        password.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL | gtk.HILDON_GTK_INPUT_MODE_INVISIBLE)
-        password.set_text(self._gconf.get_string("/apps/maemo/hermes/twitter_pwd") or '')
-        hbox.pack_start(password)
-        dialog.vbox.add(hbox)
-
-        # -- Enable is only available if both populated...
-        #
-        def _check_fields(e, p):
-            enable.set_sensitive(e.get_text() != '' and p.get_text() != '')
-        username.connect('changed', _check_fields, password)
-        password.connect('changed', _check_fields, username)
-        _check_fields(username, password)
-
-        # -- Run the dialogue...
-        #
-        dialog.show_all()
-        result = dialog.run()
-        dialog.hide()
-        if result == gtk.RESPONSE_CANCEL or result == gtk.RESPONSE_DELETE_EVENT:
-            return None
-        self._gconf.set_string("/apps/maemo/hermes/twitter_user", username.get_text())
-        self._gconf.set_string("/apps/maemo/hermes/twitter_pwd", password.get_text())
-        return result == gtk.RESPONSE_YES
 
 
     # -----------------------------------------------------------------------
-    def get_account_detail(self):
-        """Return the Twitter username."""
+    def get_urls(self):
+        """Return the various URLs needed for OAuth."""
+           
+        return ('https://api.twitter.com/oauth/request_token',
+                'https://api.twitter.com/oauth/access_token',
+                'https://api.twitter.com/oauth/authorize')
         
-        return self._gconf.get_string("/apps/maemo/hermes/twitter_user")
-    
     
     # -----------------------------------------------------------------------
     def service(self, gui_callback):
@@ -102,9 +44,9 @@ class Provider(org.maemo.hermes.engine.provider.Provider):
         
            See Service for more details."""
     
-        username = self._gconf.get_string("/apps/maemo/hermes/twitter_user") or ''
-        password = self._gconf.get_string("/apps/maemo/hermes/twitter_pwd") or ''
-        
-        api = TwitterApi(username, password)
+        return org.maemo.hermes.engine.twitter.service.Service(self.get_id(), self._api)
 
-        return org.maemo.hermes.engine.twitter.service.Service(self.get_id(), api)
+
+    # -----------------------------------------------------------------------
+    def verify_verifier(self, access_token):
+        return self._api.get_user()