ffed48996698fcf89513023cdb4b2789c084384c
[hermes] / package / src / hermes.py
1 import os.path
2 import evolution
3 from facebook import Facebook, FacebookError
4 import twitter
5 import unicodedata
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):
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
50     self.gc       = gnome.gconf.client_get_default()
51     self.callback = callback
52     self.twitter  = twitter
53     self.facebook = facebook
54
55     # -- Check the environment is going to work...
56     #
57     if (self.gc.get_string('/desktop/gnome/url-handlers/http/command') == 'epiphany %s'):
58       raise Exception('Browser in gconf invalid (see NB#136012). Installation error.')
59
60     # -- Get private keys for this app...
61     #
62     key_app    = self.gc.get_string('/apps/maemo/hermes/key_app')
63     key_secret = self.gc.get_string('/apps/maemo/hermes/key_secret')
64     if (key_app is None or key_secret is None):
65       raise Exception('No Facebook application keys found. Installation error.')
66
67     self.fb = Facebook(key_app, key_secret)
68     self.fb.desktop = True
69
70
71   # -----------------------------------------------------------------------
72   def do_fb_login(self):
73     """Perform authentication against Facebook and store the result in gconf
74          for later use. Uses the 'need_auth' and 'block_for_auth' methods on
75          the callback class. The former allows a message to warn the user
76          about what is about to happen to be shown; the second is to wait
77          for the user to confirm they have logged in."""
78     self.fb.session_key = None
79     self.fb.secret = None
80     self.fb.uid = None
81     
82     self.callback.need_auth()
83     self.fb.auth.createToken()
84     self.fb.login()
85     self.callback.block_for_auth()
86     session = self.fb.auth.getSession()
87
88     self.gc.set_string('/apps/maemo/hermes/session_key', session['session_key'])
89     self.gc.set_string('/apps/maemo/hermes/secret_key', session['secret'])
90     self.gc.set_int('/apps/maemo/hermes/uid', session['uid'])
91
92
93   # -----------------------------------------------------------------------
94   def load_friends(self):
95     """Load information on the authenticated user's friends. If no user is
96        currently authenticated, prompts for a login."""
97
98     self.friends = {}
99     self.blocked_pictures = []
100     self.callback.progress(0, 0)
101     
102     # -- Get a user session and retrieve Facebook friends...
103     #
104     if self.facebook:
105       if self.fb.session_key is None:
106         self.fb.session_key = self.gc.get_string('/apps/maemo/hermes/session_key')
107         self.fb.secret = self.gc.get_string('/apps/maemo/hermes/secret_key')
108         self.fb.uid = self.gc.get_int('/apps/maemo/hermes/uid')
109
110       # Check the available session is still valid...
111       while True:
112         try:
113           if self.fb.users.getLoggedInUser() and self.fb.session_key:
114             break
115         except FacebookError:
116           pass
117         self.do_fb_login()
118
119       # Get the list of friends...
120       attrs = ['uid', 'name', 'pic_big', 'birthday_date', 'profile_url']
121       for friend in self.fb.users.getInfo(self.fb.friends.get(), attrs):
122         friend['name'] = unicodedata.normalize('NFKD', unicode(friend['name']))
123         friend['pic']  = friend[attrs[2]]
124         self.friends[friend['name']] = friend
125         if not friend['pic']:
126           self.blocked_pictures.append(friend)
127           
128     # -- Retrieve following information from Twitter...
129     #
130     if self.twitter is not None:
131       (user, passwd) = self.twitter
132       api = twitter.Api(username=user, password=passwd)
133       users = api.GetFriends()
134       for friend in api.GetFriends():
135         self.friends[friend.name] = {'name': unicodedata.normalize('NFKD', unicode(friend.name)), 'pic': friend.profile_image_url, 'birthday_date': None, 'twitter_url': 'http://twitter.com/%s' % (friend.screen_name), 'homepage' : friend.url}
136   
137     # TODO What if the user has *no* contacts?
138
139   
140   # -----------------------------------------------------------------------
141   def sync_contacts(self, resync = False):
142     """Synchronise Facebook profiles to contact database. If resync is false,
143        no existing information will be overwritten."""
144
145     # -- Find addresses...
146     #
147     print "+++ Syncing contacts..."
148     addresses = evolution.ebook.open_addressbook('default')
149     print "+++ Addressbook opened..."
150     store = ContactStore(addresses)
151     print "+++ Contact store created..."
152     self.updated = []
153     self.unmatched = []
154     self.matched = []
155     contacts = addresses.get_all_contacts()
156     contacts.sort(key=lambda obj: obj.get_name())
157     current = 0
158     maximum = len(contacts)
159     for contact in contacts:
160       current += 1
161       self.callback.progress(current, maximum)
162       found = False
163       for name in names.variants(contact.get_name()):
164         if name in self.friends:
165           friend = self.friends[name]
166           found = True
167           updated = False
168       
169           if friend['pic'] and (resync or contact.get_property('photo') is None):
170             updated = store.set_photo(contact, friend['pic']) or updated
171         
172           if friend['birthday_date'] and (resync or contact.get_property('birth-date') is None):
173             date_str = friend['birthday_date'].split('/')
174             date_str.append('0')
175             updated = store.set_birthday(contact, int(date_str[1]),
176                                                   int(date_str[0]),
177                                                   int(date_str[2])) or updated
178
179           if 'profile_url' in friend and friend['profile_url']:
180             updated = store.add_url(contact, friend['profile_url'], unique='facebook.com') or updated
181             
182           if 'twitter_url' in friend and friend['twitter_url']:
183             updated = store.add_url(contact, friend['twitter_url'], unique='twitter.com') or updated
184             
185           if 'homepage' in friend and friend['homepage']:
186             updated = store.add_url(contact, friend['homepage']) or updated
187     
188           if updated:
189             self.updated.append(contact)
190             addresses.commit_contact(contact)
191             print "Saved changes to [%s]" % (contact.get_name())
192             
193           break
194       
195       if found:
196         self.matched.append(contact)
197       else:
198         self.unmatched.append(contact)
199
200     store.close()
201