ca61849b85adb34601270340c9cb5295cdeaac74
[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(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         token = oauth.OAuthToken.from_string(response)
159         return token
160
161     
162     # -----------------------------------------------------------------------
163     def _get_authorize_url(self, token):
164         """The URL that the user should browse to, in order to authorize the 
165            application's request to access data"""
166         
167         oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.AUTHORIZE_URL)
168         return oauth_request.to_url()
169
170
171     # -----------------------------------------------------------------------
172     def _get_access_token(self, token, verifier):
173         """If the verifier (which was displayed in the browser window) is 
174            valid, then an access token is returned which should be used to 
175            access data on the service."""
176         
177         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, verifier=verifier, http_url=self.ACCESS_TOKEN_URL)
178         oauth_request.sign_request(self.sig_method, self.consumer, token)
179
180         connection = httplib.HTTPSConnection(self.LI_SERVER)
181         connection.request(oauth_request.http_method, self.ACCESS_TOKEN_URL, headers=oauth_request.to_header()) 
182         response = connection.getresponse()
183         token_str = response.read()
184         if "ouath_problem" in token_str:
185             raise Exception("Authorization failure - failed to get access token")
186         self._store_access_token_in_gconf(token_str)
187         return oauth.OAuthToken.from_string(token_str)
188
189
190     # -----------------------------------------------------------------------
191     def _verify_verifier(self, request_token, verifier):
192         try:
193             self.access_token = self._get_access_token(request_token, verifier)
194         except Exception, e:
195             import traceback
196             traceback.print_exc()
197             raise Exception("LinkedIn authorization failed, try again (" + e + ")")
198
199
200     # -----------------------------------------------------------------------
201     def _verify_browser_command(self):
202         # -- Check the environment is going to work...
203         # FIXME: duplication
204         if (self._gc.get_string('/desktop/gnome/url-handlers/http/command') == 'epiphany %s'):
205             raise Exception('Browser in gconf invalid (see NB#136012). Installation error.')
206
207
208     # -----------------------------------------------------------------------
209     def _store_access_token_in_gconf(self, token_str):
210         self._gc.set_string(LinkedInApi.GCONF_ACCESS_TOKEN, token_str)
211
212         
213     # -----------------------------------------------------------------------
214     def get_access_token_from_gconf(self):
215         """Returns an oauth.OAuthToken, or None if the gconf value is empty"""
216         
217         token_str = self._gc.get_string(LinkedInApi.GCONF_ACCESS_TOKEN)
218         if not token_str:
219             return None
220         if "oauth_problem" in token_str:
221             self._store_access_token_in_gconf("")
222             raise Exception("Authorization failure - access token reported OAuth problem")
223         return oauth.OAuthToken.from_string(token_str)
224
225
226     # -----------------------------------------------------------------------
227     def remove_access_token_from_gconf(self):
228         """Remove the oauth.OAuthToken, if any."""
229         
230         self._gc.unset(LinkedInAPI.GCONF_ACCESS_TOKEN)