Handle variants of Joseph, Joanna, Joanne.
[hermes] / package / src / hermes.py
1 import os.path
2 import evolution
3 from facebook import Facebook, FacebookError
4 import twitter
5 import trans
6 import gnome.gconf
7 from contacts import ContactStore
8 import names
9
10 class Hermes:
11     """Encapsulate the process of syncing Facebook friends' information with the
12        Evolution contacts' database. This should be used as follows:
13        
14          * Initialise, passing in a callback (methods: need_auth(),
15            block_for_auth(), use_twitter(), use_facebook()).
16          * Call load_friends().
17          * Call sync_contacts().
18          * Retrieve information on changes effected.
19          
20        This requires two gconf paths to contain Facebook application keys:
21            /apps/maemo/hermes/key_app
22            /apps/maemo/hermes/key_secret
23          
24        Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
25        Released under the Artistic Licence."""
26     
27     
28     # -----------------------------------------------------------------------
29     def __init__(self, callback, twitter = None, facebook = False, empty = False):
30         """Constructor. Passed a callback which must implement three informational
31            methods:
32            
33              need_auth() - called to indicate a login is about to occur. The user
34                            should be informed.
35                            
36              block_for_auth() - prompt the user to take some action once they have
37                                 successfully logged in to Facebook.
38                               
39              progress(i, j) - the application is currently processing friend 'i' of
40                               'j'. Should be used to provide the user a progress bar.
41                               
42           Other parameters:
43              twitter - a username/password tuple or None if Twitter should not be
44                        used. Defaults to None.
45                        
46              facebook - boolean indicating if Facebook should be used. Defaults to
47                         False.
48         
49              empty - boolean indicating if 'empty' contacts consisting of a profile
50                      URL and birthday should be created.
51                               """
52         
53         self.gc       = gnome.gconf.client_get_default()
54         self.callback = callback
55         self.twitter  = twitter
56         self.facebook = facebook
57         self.create_empty = empty
58         
59         # -- Check the environment is going to work...
60         #
61         if (self.gc.get_string('/desktop/gnome/url-handlers/http/command') == 'epiphany %s'):
62             raise Exception('Browser in gconf invalid (see NB#136012). Installation error.')
63         
64         # -- Get private keys for this app...
65         #
66         key_app    = self.gc.get_string('/apps/maemo/hermes/key_app')
67         key_secret = self.gc.get_string('/apps/maemo/hermes/key_secret')
68         if key_app is None or key_secret is None:
69             raise Exception('No Facebook application keys found. Installation error.')
70         
71         self.fb = Facebook(key_app, key_secret)
72         self.fb.desktop = True
73     
74     
75     # -----------------------------------------------------------------------
76     def do_fb_login(self):
77         """Perform authentication against Facebook and store the result in gconf
78              for later use. Uses the 'need_auth' and 'block_for_auth' methods on
79              the callback class. The former allows a message to warn the user
80              about what is about to happen to be shown; the second is to wait
81              for the user to confirm they have logged in."""
82         self.fb.session_key = None
83         self.fb.secret = None
84         self.fb.uid = None
85         
86         self.callback.need_auth()
87         self.fb.auth.createToken()
88         self.fb.login()
89         self.callback.block_for_auth()
90         session = self.fb.auth.getSession()
91         
92         self.gc.set_string('/apps/maemo/hermes/session_key', session['session_key'])
93         self.gc.set_string('/apps/maemo/hermes/secret_key', session['secret'])
94         self.gc.set_string('/apps/maemo/hermes/uid', str(session['uid']))
95     
96     
97     # -----------------------------------------------------------------------
98     def load_friends(self):
99         """Load information on the authenticated user's friends. If no user is
100            currently authenticated, prompts for a login."""
101         
102         self.friends = {}
103         self.blocked_pictures = []
104         self.callback.progress(0, 0)
105         self.friends_by_url = {}
106         
107         # -- Get a user session and retrieve Facebook friends...
108         #
109         if self.facebook:
110             print "+++ Opening Facebook..."
111             if self.fb.session_key is None:
112                 self.fb.session_key = self.gc.get_string('/apps/maemo/hermes/session_key')
113                 self.fb.secret = self.gc.get_string('/apps/maemo/hermes/secret_key')
114                 self.fb.uid = self.gc.get_string('/apps/maemo/hermes/uid')
115             
116             # Check the available session is still valid...
117             while True:
118                 try:
119                     if self.fb.users.getLoggedInUser() and self.fb.session_key:
120                         break
121                 except FacebookError:
122                     pass
123                 self.do_fb_login()
124             
125             # Get the list of friends...
126             attrs = ['uid', 'name', 'pic_big', 'birthday_date', 'profile_url', 'first_name', 'last_name', 'website']
127             for friend in self.fb.users.getInfo(self.fb.friends.get(), attrs):
128                 key = unicode(friend['name']).encode('trans')
129                 self.friends[key] = friend
130             
131                 if 'profile_url' not in friend:
132                     friend['profile_url'] = "http://www.facebook.com/profile.php?id=" + str(friend ['uid'])
133             
134                 self.friends_by_url[friend['profile_url']] = friend
135                 friend['pic']  = friend[attrs[2]]
136                 friend['account'] = 'facebook'
137             
138                 if 'website' in friend and friend['website']:
139                     friend['homepage'] = friend['website']
140             
141                 if 'pic' not in friend or not friend['pic']:
142                     self.blocked_pictures.append(friend)
143               
144               
145         # -- Retrieve following information from Twitter...
146         #
147         if self.twitter is not None:
148             print "+++ Opening Twitter..."
149             (user, passwd) = self.twitter
150             api = twitter.Api(username=user, password=passwd)
151             users = api.GetFriends()
152             for tweeter in api.GetFriends():
153                 key    = unicode(tweeter.name).encode('trans')
154                 url    = 'http://twitter.com/%s' % (tweeter.screen_name)
155                 friend = {'name':          tweeter.name, 'pic': tweeter.profile_image_url,
156                           'birthday_date': None,         'twitter_url': url,
157                           'homepage':      tweeter.url,  'account': 'twitter'}
158                 if friend['pic'].find('/default_profile') > -1:
159                     friend['pic'] = None
160               
161                 self.friends[key] = friend
162                 self.friends_by_url[url] = friend
163         
164         # TODO What if the user has *no* contacts?
165     
166     
167     # -----------------------------------------------------------------------
168     def sync_contacts(self, resync = False):
169         """Synchronise Facebook profiles to contact database. If resync is false,
170            no existing information will be overwritten."""
171         
172         # -- Find addresses...
173         #
174         print "+++ Syncing contacts..."
175         self.addresses = evolution.ebook.open_addressbook('default')
176         print "+++ Addressbook opened..."
177         self.store = ContactStore(self.addresses)
178         print "+++ Contact store created..."
179         self.updated = []
180         self.unmatched = []
181         self.matched = []
182         contacts = self.addresses.get_all_contacts()
183         contacts.sort(key=lambda obj: obj.get_name())
184         current = 0
185         maximum = len(contacts)
186         for contact in contacts:
187             current += 1
188             self.callback.progress(current, maximum)
189             found = False
190             updated = False
191             
192             # Try match on existing URL...
193             for url in self.store.get_urls(contact):
194                 if url in self.friends_by_url:
195                     updated = self.update_contact(contact, self.friends_by_url[url], resync)
196                     found = True
197                     if updated:
198                         break
199             
200             # Fallback to names...
201             if not found:
202                 for name in names.variants(contact.get_name()):
203                     if name in self.friends:
204                         updated = self.update_contact(contact, self.friends[name], resync)
205                         found = True
206                         if updated:
207                             break
208             
209             # Keep track of updated stuff...
210             if updated:
211                 self.updated.append(contact)
212                 self.addresses.commit_contact(contact)
213                 print "Saved changes to [%s]" % (contact.get_name())
214             
215             if found:
216                 self.matched.append(contact)
217             else:
218                 self.unmatched.append(contact)
219         
220         # -- Create 'empty' contacts with birthdays...
221         #
222         if self.create_empty:
223             for name in self.friends:
224                 friend = self.friends[name]
225                 if 'contact' in friend or 'birthday_date' not in friend or not friend['birthday_date']:
226                     continue
227                 
228                 contact = evolution.ebook.EContact()
229                 contact.props.full_name = friend['name']
230                 contact.props.given_name = friend['first_name']
231                 contact.props.family_name = friend['last_name']
232                 
233                 self.update_contact(contact, friend)
234                 
235                 self.addresses.add_contact(contact)
236                 self.updated.append(contact)
237                 self.addresses.commit_contact(contact)
238                 
239                 print "Created [%s]" % (contact.get_name())
240                 self.matched.append(contact)
241         
242         self.store.close()
243     
244     
245     # -----------------------------------------------------------------------
246     def update_contact(self, contact, friend, resync = False):
247         """Update the given contact with information from the 'friend'
248            dictionary."""
249         
250         updated = False
251         friend['contact'] = contact
252           
253         if 'pic' in friend and friend['pic'] and (resync or contact.get_property('photo') is None):
254             updated = self.store.set_photo(contact, friend['pic']) or updated
255             
256         if 'birthday_date' in friend and friend['birthday_date'] and (resync or contact.get_property('birth-date') is None):
257             date_str = friend['birthday_date'].split('/')
258             date_str.append('0')
259             updated = self.store.set_birthday(contact, int(date_str[1]),
260                                                        int(date_str[0]),
261                                                        int(date_str[2])) or updated
262         
263         if 'profile_url' in friend and friend['profile_url']:
264             updated = self.store.add_url(contact, friend['profile_url'], unique='facebook.com') or updated
265                 
266         if 'twitter_url' in friend and friend['twitter_url']:
267             updated = self.store.add_url(contact, friend['twitter_url'], unique='twitter.com') or updated
268                 
269         if 'homepage' in friend and friend['homepage']:
270             updated = self.store.add_url(contact, friend['homepage']) or updated
271         
272         return updated 
273