Refactor improvements from Fredrik Wendt; and initial LinkedIn and
[hermes] / package / src / org / maemo / hermes / engine / linkedin / service.py
1 import httplib
2 import gnome.gconf
3 from oauth import oauth
4 from xml.dom.minidom import parseString
5
6 import org.maemo.hermes.engine.service
7 from org.maemo.hermes.engine.friend import Friend
8
9 from org.maemo.hermes.engine.names import canonical
10
11 #    httplib.HTTPSConnection.debuglevel = 1
12
13 class Service(org.maemo.hermes.engine.service.Service):
14     """LinkedIn backend for Hermes.
15                 
16        This sets up two gconf paths to contain LinkedIn OAuth keys:
17            /apps/maemo/hermes/linkedin_oauth
18            /apps/maemo/hermes/linkedin_verifier
19        
20        Copyright (c) Fredrik Wendt <fredrik@wendt.se> 2010.
21        Released under the Artistic Licence."""
22        
23        
24     LI_SERVER = "api.linkedin.com"
25     LI_API_URL = "https://api.linkedin.com"
26     LI_CONN_API_URL = LI_API_URL + "/v1/people/~/connections"
27
28     REQUEST_TOKEN_URL = LI_API_URL + "/uas/oauth/requestToken"
29     AUTHORIZE_URL = LI_API_URL + "/uas/oauth/authorize"
30     ACCESS_TOKEN_URL = LI_API_URL + "/uas/oauth/accessToken"
31
32
33     # -----------------------------------------------------------------------
34     def __init__(self, autocreate=False, gui_callback=None):
35         """Initialize the LinkedIn service, finding LinkedIn API keys in gconf and
36            having a gui_callback available."""
37         
38         self._gc = gnome.gconf.client_get_default()
39         self._gui = gui_callback
40         self._autocreate = autocreate
41         
42         # -- Check the environment is going to work...
43         #
44         if (self._gc.get_string('/desktop/gnome/url-handlers/http/command') == 'epiphany %s'):
45             raise Exception('Browser in gconf invalid (see NB#136012). Installation error.')
46
47         api_key = self._gc.get_string('/apps/maemo/hermes/linkedin_api_key')
48         secret_key = self._gc.get_string('/apps/maemo/hermes/linkedin_key_secret')
49
50         # FIXME: remove this
51         api_key = '1et4G-VtmtqNfY7gF8PHtxMOf0KNWl9ericlTEtdKJeoA4ubk4wEQwf8lSL8AnYE'
52         secret_key = 'uk--OtmWcxER-Yh6Py5p0VeLPNlDJSMaXj1xfHILoFzrK7fM9eepNo5RbwGdkRo_'
53
54         if api_key is None or secret_key is None:
55             raise Exception('No LinkedIn application keys found. Installation error.')
56
57         self.api_key = api_key
58         self.secret_key = secret_key
59
60         # FIXME: move this
61         token_str = "oauth_token_secret=60f817af-6437-4015-962f-cc3aefee0264&oauth_token=f89c2b7b-1c12-4f83-a469-838e78901716"
62         self.access_token = oauth.OAuthToken.from_string(token_str)
63
64         self.consumer = oauth.OAuthConsumer(api_key, secret_key)
65         self.sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
66     
67         self._friends = None
68         self._friends_by_url = {}
69         self._friends_by_contact = {}
70         
71         self.get_friends()
72
73
74     # -----------------------------------------------------------------------
75     def get_name(self):
76         return "LinkedIn"
77
78
79     # -----------------------------------------------------------------------
80     def get_friends(self):
81         """Return a list of friends from this service, or 'None' if manual mapping
82            is not supported."""
83            
84         if self._friends:
85             return self._friends.values()
86
87         # FIXME: Check the available OAuth session is still valid...
88
89         # get data
90         self._friends = {}
91         
92         xml = self._make_api_request(self.LI_CONN_API_URL)
93         dom = parseString(xml)
94         
95         self._parse_dom(dom)
96
97         print "get_friends found", len(self._friends)
98         return self._friends.values()
99
100     
101     
102     # -----------------------------------------------------------------------
103     def pre_process_contact(self, contact):
104         """Makes sure that if the contact has been mapped to a friend before,
105            remove the friend from "new friends" list."""
106         
107         for url in contact.get_urls():
108             if url in self._friends_by_url:
109                 matched_friend = self._friends_by_url[url]
110                 print "Contact is known as a friend on this service (by URL) %s - %s" % (url, matched_friend)
111                 self._friends_by_contact[contact] = matched_friend
112
113     
114     # -----------------------------------------------------------------------
115     def process_contact(self, contact, friend):
116         """Updates friend if the contact can be mapped to a friend on LinkedIn, 
117            either by previous mapping or by identifiers."""
118         
119         if self._friends_by_contact.has_key(contact):
120             friend.update(self._friends_by_contact[contact])
121         else:
122             self._match_contact_by_identifiers(contact, friend)
123     
124
125     # -----------------------------------------------------------------------
126     def finalise(self, updated):
127         if self._autocreate or True:
128             for f in self._friends.values():
129                 if f not in self._friends_by_contact.values():
130                     # FIXME: create friends as contact
131                     print "Could/should create contact here for %s" % f
132
133
134     # -----------------------------------------------------------------------
135     def _get_access_token(self, token, verifier):
136         """user provides the verifier, which was displayed in the browser window"""
137         
138         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, verifier=verifier, http_url=self.ACCESS_TOKEN_URL)
139         oauth_request.sign_request(self.sig_method, self.consumer, token)
140
141         connection = httplib.HTTPSConnection(self.LI_SERVER)
142         connection.request(oauth_request.http_method, self.ACCESS_TOKEN_URL, headers=oauth_request.to_header()) 
143         response = connection.getresponse()
144         return oauth.OAuthToken.from_string(response.read())
145
146
147     # -----------------------------------------------------------------------
148     def _get_authorize_url(self, token):
149         """The URL that the user should browse to, in order to authorize the application to acess data"""
150         
151         oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.AUTHORIZE_URL)
152         return oauth_request.to_url()
153
154
155     # -----------------------------------------------------------------------
156     def _get_request_token(self, callback):
157         """Get a request token from LinkedIn"""
158         
159         oauth_consumer_key = self.api_key
160         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, callback=callback, http_url=self.REQUEST_TOKEN_URL)
161         oauth_request.sign_request(self.sig_method, self.consumer, None)
162
163         connection = httplib.HTTPSConnection(self.LI_SERVER)
164         connection.request(oauth_request.http_method, self.REQUEST_TOKEN_URL, headers=oauth_request.to_header())
165         response = self.connection.getresponse().read()
166         
167         token = oauth.OAuthToken.from_string(response)
168         return token
169
170
171     # -----------------------------------------------------------------------
172     def _make_api_request(self, url):
173         print "_make_api_request", url
174         oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
175         oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
176         connection = httplib.HTTPSConnection(self.LI_SERVER)
177         try:
178             connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
179             return connection.getresponse().read()
180         except:
181             raise Exception("Failed to contact LinkedIn at " + url)
182
183
184     # -----------------------------------------------------------------------
185     def _do_li_login(self):
186         """Perform authentication against LinkedIn and store the result in gconf
187              for later use. Uses the 'need_auth' and 'block_for_auth' methods on
188              the callback class. The former allows a message to warn the user
189              about what is about to happen to be shown; the second is to wait
190              for the user to confirm they have logged in."""
191         # FIXME
192
193
194     # -----------------------------------------------------------------------
195     def _match_contact_by_identifiers(self, contact, friend):
196         for id in contact.get_identifiers():
197             if id in self._friends:
198                 matched_friend = self._friends[id]
199                 if matched_friend in self._friends_by_contact.values():
200                     print "Avoiding assigning same friend to two contacts: %s " % matched_friend
201                 else:
202                     self._friends_by_contact[contact] = matched_friend
203                     friend.update(matched_friend)
204
205
206     # -----------------------------------------------------------------------
207     def _parse_dom(self, dom):
208         print "parse_dom", dom
209         def get_first_tag(node, tagName):
210             tags = node.getElementsByTagName(tagName)
211             if tags and len(tags) > 0:
212                 return tags[0]
213         
214         def extract(node, tagName):
215             tag = get_first_tag(node, tagName)
216             if tag:
217                 return tag.firstChild.nodeValue
218             
219         def extract_public_url(node):
220             tag = get_first_tag(node, 'site-standard-profile-request')
221             if tag:
222                 url = extract(tag, 'url')
223                 return url.replace("&amp;", "&")
224         
225         # FIXME: look for <error>
226         people = dom.getElementsByTagName('person')
227         for p in people:
228  #           try:
229             if True:
230                 fn = extract(p, 'first-name')
231                 ln = extract(p, 'last-name')
232                 photo_url = extract(p, 'picture-url')
233                 id = extract(p, 'id')
234                 public_url = extract_public_url(p)
235
236                 name = fn + " " + ln
237                 friend = Friend(name)
238                 friend.add_url(public_url)
239                 if photo_url: friend.set_photo_url(photo_url)
240
241                 key = canonical(name)
242                 self._friends[key] = friend
243                 self._friends_by_url[public_url] = friend
244 #            except:
245 #                continue