76329ef312d451fd4aa98b6caf732ea7b139a4fb
[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