0d9eb2429eb763f022471954e01824c130dbf7f6
[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         self._verify_browser_command()
47
48
49     # -----------------------------------------------------------------------
50     def authenticate(self, need_auth, block_for_auth):
51         need_auth()
52         token = self._get_request_token()
53         url = self._get_authorize_url(token)
54         verifier = block_for_auth(True)
55         self._verify_verifier(token, verifier)
56     
57     
58     # -----------------------------------------------------------------------
59     def get_friends(self):
60         """ Returns a Friend object for each LinkedIn connection."""
61         
62         xml = self._make_api_request(self.LI_CONN_API_URL)
63         dom = parseString(xml)
64         friends = self._parse_dom(dom)
65
66         return friends
67         
68
69     # -----------------------------------------------------------------------
70     def get_friend_details(self, url, header_value):
71         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
72         oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
73
74         headers = oauth_request.to_header()
75         headers[u'x-li-auth-token'] = header_value
76         connection = httplib.HTTPConnection("api.linkedin.com")
77         connection.request(oauth_request.http_method, url, headers=headers)
78         data = connection.getresponse().read()
79         return data
80     
81
82     # -----------------------------------------------------------------------
83     def _make_api_request(self, url):
84         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
85         oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
86         connection = httplib.HTTPSConnection(self.LI_SERVER)
87         try:
88             connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
89             xml = connection.getresponse().read()
90             return xml
91         except:
92             raise Exception("Failed to contact LinkedIn at " + url)
93
94
95     # -----------------------------------------------------------------------
96     def _parse_dom(self, dom):
97         def get_first_tag(node, tagName):
98             tags = node.getElementsByTagName(tagName)
99             if tags and len(tags) > 0:
100                 return tags[0]
101         
102         def extract(node, tagName):
103             tag = get_first_tag(node, tagName)
104             if tag:
105                 return tag.firstChild.nodeValue
106             
107         def extract_public_url(node):
108             tag = get_first_tag(node, 'site-standard-profile-request')
109             if tag:
110                 url = extract(tag, 'url')
111                 return url.replace("&amp;", "&")
112         
113         # look for errors
114         errors = dom.getElementsByTagName('error')
115         if (len(errors) > 0):
116             details = ""
117             try:
118                 details = " (" + extract(errors[0], "message") + ")"
119             except:
120                 pass
121             raise Exception("LinkedIn communication errors detected" + details)
122         
123         friends = []
124         people = dom.getElementsByTagName('person')
125         for p in people:
126             try:
127                 fn = extract(p, 'first-name')
128                 ln = extract(p, 'last-name')
129                 photo_url = extract(p, 'picture-url')
130                 id = extract(p, 'id')
131                 public_url = extract_public_url(p)
132
133                 name = fn + " " + ln
134                 friend = Friend(name)
135                 friend.add_url(public_url)
136                 if photo_url:
137                     friend.set_photo_url(photo_url)
138                 
139                 friends.append(friend)
140
141             except:
142                 pass
143
144         return friends
145
146     # -----------------------------------------------------------------------
147     def _get_request_token(self):
148         """Get a request token from LinkedIn"""
149         
150         oauth_consumer_key = self.api_key
151         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, callback="oob", http_url=self.REQUEST_TOKEN_URL)
152         oauth_request.sign_request(self.sig_method, self.consumer, None)
153
154         connection = httplib.HTTPSConnection(self.LI_SERVER)
155         connection.request(oauth_request.http_method, self.REQUEST_TOKEN_URL, headers=oauth_request.to_header())
156         response = connection.getresponse().read()
157         
158         try:
159             token = oauth.OAuthToken.from_string(response)
160         except Exception, e:
161             raise Exception("Authorization failure - failed to get request token")
162         return token
163
164     
165     # -----------------------------------------------------------------------
166     def _get_authorize_url(self, token):
167         """The URL that the user should browse to, in order to authorize the 
168            application's request to access data"""
169         
170         oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.AUTHORIZE_URL)
171         return oauth_request.to_url()
172
173
174     # -----------------------------------------------------------------------
175     def _get_access_token(self, token, verifier):
176         """If the verifier (which was displayed in the browser window) is 
177            valid, then an access token is returned which should be used to 
178            access data on the service."""
179         
180         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, verifier=verifier, http_url=self.ACCESS_TOKEN_URL)
181         oauth_request.sign_request(self.sig_method, self.consumer, token)
182
183         connection = httplib.HTTPSConnection(self.LI_SERVER)
184         connection.request(oauth_request.http_method, self.ACCESS_TOKEN_URL, headers=oauth_request.to_header()) 
185         response = connection.getresponse()
186         token_str = response.read()
187         if "ouath_problem" in token_str:
188             raise Exception("Authorization failure - failed to get access token")
189         self._store_access_token_in_gconf(token_str)
190         return oauth.OAuthToken.from_string(token_str)
191
192
193     # -----------------------------------------------------------------------
194     def _verify_verifier(self, request_token, verifier):
195         try:
196             self.access_token = self._get_access_token(request_token, verifier)
197         except Exception, e:
198             import traceback
199             traceback.print_exc()
200             raise Exception("LinkedIn authorization failed, try again (" + e + ")")
201
202
203     # -----------------------------------------------------------------------
204     def _verify_browser_command(self):
205         # -- Check the environment is going to work...
206         # FIXME: duplication
207         if (self._gc.get_string('/desktop/gnome/url-handlers/http/command') == 'epiphany %s'):
208             raise Exception('Browser in gconf invalid (see NB#136012). Installation error.')
209
210
211     # -----------------------------------------------------------------------
212     def _store_access_token_in_gconf(self, token_str):
213         self._gc.set_string(LinkedInApi.GCONF_ACCESS_TOKEN, token_str)
214
215         
216     # -----------------------------------------------------------------------
217     def get_access_token_from_gconf(self):
218         """Returns an oauth.OAuthToken, or None if the gconf value is empty"""
219         
220         token_str = self._gc.get_string(LinkedInApi.GCONF_ACCESS_TOKEN)
221         if not token_str:
222             return None
223         if "oauth_problem" in token_str:
224             self._store_access_token_in_gconf("")
225             raise Exception("Authorization failure - access token reported OAuth problem")
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)