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