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