# Python module that handles calls to the twitter API as a separate thread import re import gtk, gobject from threading import Thread,RLock import twitter_pb2 from oauthtwitter import OAuthApi from urllib2 import HTTPError,URLError import avcache import webbrowser from time import sleep # These are the global constants for the twitter oauth entry for our app CONSUMER_KEY = 'jGu64TPCUtyLZKyWyMJldQ' CONSUMER_SECRET = 'lTRrTyvzRfJab5HsAe16zkV8tqFVRp0k0cTfHL0l4GE' class CustomApi(OAuthApi): ''' This is a Twitter API with an RLock for multi-threaded access Also included is logic for processing the list names when the object is instantiated ''' def __init__(self, access_token): OAuthApi.__init__(self, CONSUMER_KEY, CONSUMER_SECRET, access_token) self.lock = RLock() self.sig_proxy = SigProxy() self.username = self.GetUserInfo().screen_name thread = GetUserLists(api=self) thread.sig_proxy.connect('lists-ready', self.on_lists_ready) thread.start() def on_lists_ready(self, widget, lists, ignored): list_names = [] for l in lists['lists']: list_names.append(l.name) list_names.sort() self.sig_proxy.emit('lists-ready', self.username, list_names) # End class CustomApi class ApiThread(Thread): def __init__(self, api): Thread.__init__(self) self.api = api self.setDaemon(True) # End class ApiThread class GetTweets(ApiThread): def __init__(self, api, list_name, pane, num_entries, username): ApiThread.__init__(self, api) self.list_name = list_name self.pane = pane self.num_entries = num_entries self.username = username def run(self): try: # username/Home entries need to load the appropriate Home feed if self.list_name == self.username + '/Home': with self.api.lock: statuses = self.api.GetHomeTimeline(count=self.num_entries) # For @username, check if it is one of our usernames, or # just needs to be searched on elif self.list_name == '@' + self.username: with self.api.lock: statuses = self.api.GetMentions(count=self.num_entries) elif re.match('@', self.list_name): with self.api.lock: results = self.api.Search(self.list_name, rpp=self.num_entries) statuses = results_to_statuses(results, self.api) # Direct Messages should match like /Home, above elif self.list_name == self.username + '/Direct Messages': with self.api.lock: dms = self.api.GetDirectMessages() statuses = dms_to_statuses(dms) # User lookups go straight to the user elif re.match(r'user: ', self.list_name): with self.api.lock: statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', self.list_name), count=self.num_entries) # Lists load the appropriate list from the appropriate account elif re.match(r'list: ', self.list_name): real_list = re.sub(r'list: .*/(.*)', r'\1', self.list_name) with self.api.lock: statuses = self.api.GetListStatuses(real_list, per_page=self.num_entries) # Everything else is a straight search else: with self.api.lock: results = self.api.Search(self.list_name, rpp=self.num_entries) statuses = results_to_statuses(results, self.api) except (HTTPError, URLError): statuses = None # For each user id present, populate the AvCache with an image if statuses: for status in statuses: avcache.add_to_cache(status.user) gtk.gdk.threads_enter() try: self.pane.update_window(statuses) finally: gtk.gdk.threads_leave() ### End class GetTweets class GetSingleTweet(ApiThread): def __init__(self, api, pane, single_tweet): ApiThread.__init__(self, api) self.pane = pane self.single_tweet = single_tweet def run(self): statuses = [] with self.api.lock: try: statuses.append(self.api.GetStatus(self.single_tweet)) except (HTTPError, URLError): statuses = None # In case we've never seen this user, grab their profile image for status in statuses: avcache.add_to_cache(status.user) gtk.gdk.threads_enter() try: self.pane.update_window(statuses) finally: gtk.gdk.threads_leave() ### End class GetSingleTweet class GetConversation(ApiThread): def __init__(self, api, pane, root_tweet_id): ApiThread.__init__(self, api) self.pane = pane self.root_tweet_id = root_tweet_id def run(self): statuses = [] last_tweet = None # Get the root tweet try: with self.api.lock: last_tweet = self.api.GetStatus(self.root_tweet_id) statuses.append(last_tweet) except (HTTPError, URLError): statuses = None last_tweet = None # get tweets in a loop, until there is no in_reply_to_status_id while last_tweet and last_tweet.in_reply_to_status_id: try: with self.api.lock: last_tweet = self.api.GetStatus(last_tweet.in_reply_to_status_id) statuses.append(last_tweet) except (HTTPError, URLError): last_tweet = None # In case we've never seen some of these users, grab their profile images and cache them for status in statuses: avcache.add_to_cache(status.user) statuses.reverse() gtk.gdk.threads_enter() try: self.pane.update_window(statuses) finally: gtk.gdk.threads_leave() ### End class GetConversation class GetFollowing(ApiThread): def __init__(self, api, pane, user): ApiThread.__init__(self, api) self.pane = pane self.user = user def run(self): screen_name = re.sub('user: ', '', self.user) try: with self.api.lock: relationship = self.api.ShowFriendships(target_screen_name=screen_name) following = relationship.source.following except (HTTPError, URLError): following = False self.pane.user_box.set_following(following) ### End class GetFollowing class GetUserInfo(ApiThread): def __init__(self, api, pane, user): ApiThread.__init__(self, api) self.pane = pane self.user = user def run(self): screen_name = re.sub('user: ', '', self.user) try: with self.api.lock: user = self.api.GetUser(screen_name) except (HTTPError, URLError): verified = False avcache.add_to_cache(user) gtk.gdk.threads_enter() try: self.pane.user_box.update_info(user) finally: gtk.gdk.threads_leave() ### End class GetUserInfo class GetUserLists(ApiThread): def __init__(self, api): ApiThread.__init__(self, api) self.sig_proxy = SigProxy() def run(self): lists = [] done = False while not done: done = True try: with self.api.lock: lists = self.api.GetUserLists() except (HTTPError, URLError): done = False self.sig_proxy.emit('lists-ready', lists, None) ### End class GetUserLists class PostUpdate(ApiThread): def __init__(self, api, update, reply_id): ApiThread.__init__(self, api) self.sig_proxy = SigProxy() self.update = update self.reply_id = reply_id def run(self): try: with self.api.lock: self.api.PostUpdate(self.update, in_reply_to_status_id=self.reply_id) success = True except (HTTPError, URLError): success = False self.sig_proxy.emit('update-posted', success) ### End class PostUpdate class PostRetweet(ApiThread): def __init__(self, api, retweet_id): ApiThread.__init__(self, api) self.sig_proxy = SigProxy() self.retweet_id = retweet_id def run(self): try: with self.api.lock: self.api.PostRetweet(self.retweet_id) success = True except (HTTPError, URLError): success = False self.sig_proxy.emit('retweet-posted', success) ### End class PostRetweet class ChangeFriendship(ApiThread): def __init__(self, api, pane, user_name, follow=True): ApiThread.__init__(self, api) self.sig_proxy = SigProxy() self.user_name = user_name self.follow = follow self.pane = pane def run(self): try: with self.api.lock: if self.follow: user = self.api.CreateFriendship(self.user_name) else: user = self.api.DestroyFriendship(self.user_name) if user.__class__ == twitter_pb2.User: success = True else: success = False except (HTTPError, URLError): success = False gtk.gdk.threads_enter() try: if self.follow: self.pane.user_box.set_following(success) else: self.pane.user_box.set_following(not success) finally: gtk.gdk.threads_leave() self.sig_proxy.emit('friendship-changed', {'user_name': self.user_name, 'follow': self.follow, 'success': success}) ### End class ChangeFriendship class SigProxy(gtk.Alignment): """ This little class exists just so that we can have a gtk class in our threads that can emit signals. That way, we can communicate data back to the gtk interface easily. """ def __init__(self): gtk.Alignment.__init__(self) # End class SigProxy gobject.signal_new("lists-ready", SigProxy, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,gobject.TYPE_PYOBJECT)) gobject.signal_new("update-posted", SigProxy, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("retweet-posted", SigProxy, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) gobject.signal_new("friendship-changed", SigProxy, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) # We use these classes to emulate a Status object when we need # one to be built out of something else. class User(): def __init__(self): self.screen_name = None self.name = None self.profile = Profile() class Profile(): def __init__(self): self.image_url = None class Status(): def __init__(self): self.user = User() self.id = None self.created_at = None self.in_reply_to_screen_name = None self.in_reply_to_status_id = None def results_to_statuses(results, api): """ Since the REST API and the Search API return different result types, this function converts Search API Results into custom-baked Status objects that mimic those returned by the REST API. To get the 'in reply to' data, it has to grab the individual tweet for any status that has a to_user_id field set. This can slow things down a lot. fixme: add an option to disable this for speed... """ statuses = [] for result in results.results: status = Status() status.id = result.id status.user = User() status.user.screen_name = result.from_user status.user.name = "" status.user.profile.image_url = result.profile_image_url status.in_reply_to_screen_name = result.to_user # The Twitter Search API has different timestamps than the # REST API... balls # fixme: # Gotta be a cleaner way to do this, but I can't think of it # right now created_at = re.sub(',', '', result.created_at) created_split = re.split(' ', created_at) status.created_at = created_split[0] + ' ' + created_split[2] + ' ' + created_split[1] + ' ' + created_split[4] + ' ' + created_split[5] + ' ' + created_split[3] status.text = result.text # If this is in reply to something, get the relevant tweet if result.to_user_id is not None: try: with api.lock: tweet = api.GetStatus(result.id) status.in_reply_to_status_id = tweet.in_reply_to_status_id except (HTTPError, URLError): pass # Just move along, leave off the 'in reply to' data statuses.append(status) return statuses def dms_to_statuses(direct_messages): """ To make it easier for our widgets to handle, we convert Direct Message results to our home-baked Status objects that mimic the REST API Statuses. """ statuses = [] for dm in direct_messages: status = Status() status.id = dm.id status.user = User() status.user.screen_name = dm.sender_screen_name status.user.name = dm.sender.name status.user.profile.image_url = dm.sender.profile.image_url status.created_at = dm.created_at status.text = dm.text statuses.append(status) return statuses def get_access_token(window): auth_api = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET) request_token = auth_api.getRequestToken() authorization_url = auth_api.getAuthorizationURL(request_token) webbrowser.open(authorization_url) auth_api = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET, request_token) dialog = gtk.Dialog("Enter PIN", window, gtk.DIALOG_MODAL, (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) entry = gtk.Entry() dialog.vbox.pack_start(entry) entry.show() response = dialog.run() dialog.hide() if response == gtk.RESPONSE_OK: pin = entry.get_text() try: access_token = auth_api.getAccessToken(pin) except HTTPError: access_token = None return access_token else: return None