This repository has been archived on 2019-12-04. You can view files and clone it, but cannot push or open issues or pull requests.
hrafn/apithreads.py

502 lines
15 KiB
Python

# 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 usercache
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
self._username = self.username
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 UserCaches
if statuses:
for status in statuses:
usercache.add_to_av_cache(status.user)
usercache.add_to_name_cache(status.user, self.api)
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:
usercache.add_to_av_cache(status.user)
usercache.add_to_name_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:
usercache.add_to_av_cache(status.user)
usercache.add_to_name_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)
usercache.add_to_av_cache(user)
usercache.add_to_name_cache(user)
except (HTTPError, URLError):
verified = False
user = None
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) as exception:
print 'debug: GetUserLists had error: ' + str(exception.code)
sleep(5)
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(object):
def __init__(self):
self.profile = Profile()
class Profile(object):
pass
class Status(object):
def __init__(self):
self.user = User()
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
status.source = result.source
# 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
status.source = None
status.in_reply_to_status_id = None
status.in_reply_to_screen_name = None
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