added url argument to block_for_auth, fixed tests for LinkedIn
[hermes] / package / src / org / maemo / hermes / engine / linkedin / api.py
1 import httplib
2 import gnome.gconf
3 from oauth import oauth
4 from xml.dom.minidom import parseString
5 from org.maemo.hermes.engine.friend import Friend
6
7 class LinkedInApi():
8     """LinkedIn API for Hermes.
9                 
10        Copyright (c) Fredrik Wendt <fredrik@wendt.se> 2010.
11        Released under the Artistic Licence."""
12        
13     GCONF_API_KEY = '/apps/maemo/hermes/linkedin_key'
14     GCONF_API_SECRET = '/apps/maemo/hermes/linkedin_secret'
15     GCONF_ACCESS_TOKEN = '/apps/maemo/hermes/linkedin_access_token'
16     
17     LI_SERVER = "api.linkedin.com"
18     LI_API_URL = "https://api.linkedin.com"
19     LI_CONN_API_URL = LI_API_URL + "/v1/people/~/connections"
20
21     REQUEST_TOKEN_URL = LI_API_URL + "/uas/oauth/requestToken"
22     AUTHORIZE_URL = LI_API_URL + "/uas/oauth/authorize"
23     ACCESS_TOKEN_URL = LI_API_URL + "/uas/oauth/accessToken"
24
25
26     # -----------------------------------------------------------------------
27     def __init__(self, gconf=None):
28         """Initialize the LinkedIn service, finding LinkedIn API keys in gconf and
29            having a gui_callback available."""
30         
31         if gconf: self._gc = gconf
32         else: self._gc = gnome.gconf.client_get_default()
33         
34         api_key = self._gc.get_string(LinkedInApi.GCONF_API_KEY)
35         secret_key = self._gc.get_string(LinkedInApi.GCONF_API_SECRET)
36         self.api_key = api_key
37
38         if api_key is None or secret_key is None:
39             raise Exception('No LinkedIn application keys found. Installation error.')
40
41         self.access_token = self.get_access_token_from_gconf()
42
43         self.consumer = oauth.OAuthConsumer(api_key, secret_key)
44         self.sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
45         
46
47     # -----------------------------------------------------------------------
48     def authenticate(self, need_auth, block_for_auth):
49         need_auth()
50         token = self._get_request_token()
51         url = self._get_authorize_url(token)
52         verifier = block_for_auth(True, url)
53         self._verify_verifier(token, verifier)
54     
55     
56     # -----------------------------------------------------------------------
57     def get_friends(self):
58         """ Returns a Friend object for each LinkedIn connection."""
59         
60         xml = self._make_api_request(self.LI_CONN_API_URL)
61         dom = parseString(xml)
62         friends = self._parse_dom(dom)
63
64         return friends
65         
66
67     # -----------------------------------------------------------------------
68     def get_friend_details(self, url, header_value):
69         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
70         oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
71
72         headers = oauth_request.to_header()
73         headers[u'x-li-auth-token'] = header_value
74         connection = httplib.HTTPConnection("api.linkedin.com")
75         connection.request(oauth_request.http_method, url, headers=headers)
76         data = connection.getresponse().read()
77         return data
78     
79
80     # -----------------------------------------------------------------------
81     def _make_api_request(self, url):
82         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
83         oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
84         connection = httplib.HTTPSConnection(self.LI_SERVER)
85         try:
86             connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
87             xml = connection.getresponse().read()
88             return xml
89         except:
90             raise Exception("Failed to contact LinkedIn at " + url)
91
92
93     # -----------------------------------------------------------------------
94     def _parse_dom(self, dom):
95         def get_first_tag(node, tagName):
96             tags = node.getElementsByTagName(tagName)
97             if tags and len(tags) > 0:
98                 return tags[0]
99         
100         def extract(node, tagName):
101             tag = get_first_tag(node, tagName)
102             if tag:
103                 return tag.firstChild.nodeValue
104             
105         def extract_public_url(node):
106             tag = get_first_tag(node, 'site-standard-profile-request')
107             if tag:
108                 url = extract(tag, 'url')
109                 return url.replace("&amp;", "&")
110         
111         # look for errors
112         errors = dom.getElementsByTagName('error')
113         if (len(errors) > 0):
114             details = ""
115             try:
116                 details = " (" + extract(errors[0], "message") + ")"
117             except:
118                 pass
119             raise Exception("LinkedIn communication errors detected" + details)
120         
121         friends = []
122         people = dom.getElementsByTagName('person')
123         for p in people:
124             try:
125                 fn = extract(p, 'first-name')
126                 ln = extract(p, 'last-name')
127                 photo_url = extract(p, 'picture-url')
128                 id = extract(p, 'id')
129                 public_url = extract_public_url(p)
130
131                 name = fn + " " + ln
132                 friend = Friend(name)
133                 friend.add_url(public_url)
134                 if photo_url:
135                     friend.set_photo_url(photo_url)
136                 
137                 friends.append(friend)
138
139             except:
140                 pass
141
142         return friends
143
144     # -----------------------------------------------------------------------
145     def _get_request_token(self):
146         """Get a request token from LinkedIn"""
147         
148         oauth_consumer_key = self.api_key
149         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, callback="oob", http_url=self.REQUEST_TOKEN_URL)
150         oauth_request.sign_request(self.sig_method, self.consumer, None)
151
152         connection = httplib.HTTPSConnection(self.LI_SERVER)
153         connection.request(oauth_request.http_method, self.REQUEST_TOKEN_URL, headers=oauth_request.to_header())
154         response = connection.getresponse().read()
155         
156         try:
157             token = oauth.OAuthToken.from_string(response)
158         except Exception, e:
159             import traceback
160             traceback.print_exc()
161             print response
162             raise Exception("Authorization failure - failed to get request token")
163         return token
164
165     
166     # -----------------------------------------------------------------------
167     def _get_authorize_url(self, token):
168         """The URL that the user should browse to, in order to authorize the 
169            application's request to access data"""
170         
171         oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.AUTHORIZE_URL)
172         return oauth_request.to_url()
173
174
175     # -----------------------------------------------------------------------
176     def _get_access_token(self, token, verifier):
177         """If the verifier (which was displayed in the browser window) is 
178            valid, then an access token is returned which should be used to 
179            access data on the service."""
180         
181         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, verifier=verifier, http_url=self.ACCESS_TOKEN_URL)
182         oauth_request.sign_request(self.sig_method, self.consumer, token)
183
184         connection = httplib.HTTPSConnection(self.LI_SERVER)
185         connection.request(oauth_request.http_method, self.ACCESS_TOKEN_URL, headers=oauth_request.to_header()) 
186         response = connection.getresponse()
187         token_str = response.read()
188         if 'oauth_problem' in token_str:
189             raise Exception("Authorization failure - failed to get access token (" + token_str + ")")
190         self._store_access_token_in_gconf(token_str)
191         return oauth.OAuthToken.from_string(token_str)
192
193
194     # -----------------------------------------------------------------------
195     def _verify_verifier(self, request_token, verifier):
196         try:
197             self.access_token = self._get_access_token(request_token, verifier)
198         except Exception, e:
199             import traceback
200             traceback.print_exc()
201             raise Exception("LinkedIn authorization failed, try again")
202
203
204     # -----------------------------------------------------------------------
205     def _store_access_token_in_gconf(self, token_str):
206         self._gc.set_string(LinkedInApi.GCONF_ACCESS_TOKEN, token_str)
207
208         
209     # -----------------------------------------------------------------------
210     def get_access_token_from_gconf(self):
211         """Returns an oauth.OAuthToken, or None if the gconf value is empty"""
212         
213         token_str = self._gc.get_string(LinkedInApi.GCONF_ACCESS_TOKEN)
214         if not token_str:
215             return None
216         if "oauth_problem" in token_str:
217             self._store_access_token_in_gconf("")
218             raise Exception("Authorization failure - access token reported OAuth problem")
219         return oauth.OAuthToken.from_string(token_str)
220
221
222     # -----------------------------------------------------------------------
223     def remove_access_token_from_gconf(self):
224         """Remove the oauth.OAuthToken, if any."""
225         
226         self._gc.unset(LinkedInAPI.GCONF_ACCESS_TOKEN)