Add additional debugging to try and identify cause of MB#11103
[hermes] / package / src / org / maemo / hermes / engine / linkedin / api.py
index 882360a..363b2e9 100644 (file)
@@ -3,57 +3,41 @@ import httplib
 import gnome.gconf
 from oauth import oauth
 from xml.dom.minidom import parseString
+from org.maemo.hermes.engine.phonenumber import PhoneNumber
 from org.maemo.hermes.engine.friend import Friend
+from org.maemo.hermes.engine.service import CredentialsExpiredException
 
 class LinkedInApi():
     """LinkedIn API for Hermes.
                 
        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"
+    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")
+
     
     
     # -----------------------------------------------------------------------
@@ -78,27 +62,15 @@ 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)
 
 
     # -----------------------------------------------------------------------
     def _parse_dom(self, dom):
         def get_first_tag(node, tagName):
-            tags = node.getElementsByTagName(tagName)
-            if tags and len(tags) > 0:
-                return tags[0]
+            if node:
+                tags = node.getElementsByTagName(tagName)
+                if tags and len(tags) > 0:
+                    return tags[0]
         
         def extract(node, tagName):
             tag = get_first_tag(node, tagName)
@@ -110,125 +82,84 @@ class LinkedInApi():
             if tag:
                 url = extract(tag, 'url').replace("&amp;", "&")
                 return re.sub('[?&](auth|trk)\w*=[^&]*', '', url)
+            
+        def extract_phone_numbers(node):
+            country = extract(get_first_tag(node, 'country'), 'code')
+            tag = get_first_tag(node, 'phone-numbers')
+            numbers = []
+            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 = []
+            if tag:
+                for element in tag.getElementsByTagName('url'):
+                    urls.append(element.firstChild.nodeValue.replace("&amp;", "&"))
+            return urls
+        
+        def extract_birthday(node):
+            tag = get_first_tag(node, 'date-of-birth')
+            bday = None
+            if tag:
+                month = extract(tag, 'month')
+                day   = extract(tag, 'day')
+                year  = extract(tag, 'year')
+                if month and day:
+                    bday = '%s/%s' % (extract(tag, 'month'), extract(tag, 'day'))
+                    if year:
+                        bday = '%s/%s' % (bday, year)
+                    
+            return bday
         
         # look for errors
         errors = dom.getElementsByTagName('error')
-        if (len(errors) > 0):
+        if errors and len(errors) > 0:
             details = ""
             try:
                 details = " (" + extract(errors[0], "message") + ")"
             except:
                 pass
-            raise Exception("LinkedIn communication errors detected" + details)
+            if details.startswith('([unauth'):
+                raise CredentialsExpiredException('linkedin')
+            else:
+                raise Exception("LinkedIn communication errors detected" + details)
         
         friends = []
         people = dom.getElementsByTagName('person')
         for p in people:
-            try:
-                fn = extract(p, 'first-name')
-                ln = extract(p, 'last-name')
-                photo_url = extract(p, 'picture-url')
-                id = extract(p, 'id')
-                public_url = extract_public_url(p)
-
-                name = fn + " " + ln
-                friend = Friend(name)
-                friend.add_url(public_url)
-                if photo_url:
-                    friend.set_photo_url(photo_url)
+            fn = extract(p, 'first-name')
+            ln = extract(p, 'last-name')
+            photo_url = extract(p, 'picture-url')
+            id = extract(p, 'id')
+            public_url = extract_public_url(p)
+            bday = extract_birthday(p)
+
+            name = fn + " " + ln
+            friend = Friend(name)
+            friend.add_url(public_url)
+            if photo_url:
+                friend.set_photo_url(photo_url)
                 
-                friends.append(friend)
-
-            except:
-                pass
+            if bday:
+                friend.set_birthday_date(bday)
+                
+            for number in extract_phone_numbers(p):
+                friend.add_phone(number)
+                
+            for url in extract_urls(p):
+                friend.add_url(url)
+            
+            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)