Merging in multithread branch now that it is stable enough
This commit is contained in:
commit
0c280eeb4d
6
TODO
6
TODO
|
@ -9,11 +9,11 @@ features:
|
|||
* Status bar icon
|
||||
* Support viewing a list of friends and unfollowing them
|
||||
* Support creating new lists and adding users to it (as well as removing users and deleting lists)
|
||||
|
||||
* Need a new way to post failed tweet retrieval to the status bar
|
||||
|
||||
bugs:
|
||||
|
||||
* Direct Messages have no names, only screen names (may not be fixable without
|
||||
considerable tweets to python-twitter)
|
||||
* Apparently, reply button isn't functioning, and some posts look like they are
|
||||
in reply to someone when the twitter website says they are not
|
||||
* Can't always kill tabs - open tab list not always correctly populated?
|
||||
* Follow/Unfollow and Verified don't get set on current tab when it first appears... need a signal to come back from the TweetPane.
|
||||
|
|
263
apithreads.py
Normal file
263
apithreads.py
Normal file
|
@ -0,0 +1,263 @@
|
|||
# Python module that handles calls to the twitter API as a separate thread
|
||||
|
||||
import re
|
||||
import gtk, gobject
|
||||
from threading import Thread,RLock
|
||||
from twitter import Api
|
||||
from urllib2 import HTTPError,URLError
|
||||
|
||||
class CustomApi(Api):
|
||||
'''
|
||||
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, username, password):
|
||||
Api.__init__(self, username, password)
|
||||
self.lock = RLock()
|
||||
self.sig_proxy = SigProxy()
|
||||
|
||||
self.username = 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):
|
||||
with self.api.lock:
|
||||
try:
|
||||
# username/Home entries need to load the appropriate Home feed
|
||||
if self.list_name == self.username + '/Home':
|
||||
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:
|
||||
statuses = self.api.GetMentions(count=self.num_entries)
|
||||
elif re.match('@', self.list_name):
|
||||
statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries))
|
||||
|
||||
# Direct Messages should match like /Home, above
|
||||
elif self.list_name == self.username + '/Direct Messages':
|
||||
statuses = dms_to_statuses(self.api.GetDirectMessages())
|
||||
|
||||
# User lookups go straight to the user
|
||||
elif re.match(r'user: ', self.list_name):
|
||||
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)
|
||||
statuses = self.api.GetListStatuses(real_list, per_page=self.num_entries)
|
||||
|
||||
# Everything else is a straight search
|
||||
else:
|
||||
statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries))
|
||||
|
||||
except (HTTPError, URLError):
|
||||
statuses = None
|
||||
|
||||
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
|
||||
|
||||
gtk.gdk.threads_enter()
|
||||
try:
|
||||
self.pane.update_window(statuses)
|
||||
finally:
|
||||
gtk.gdk.threads_leave()
|
||||
|
||||
|
||||
### End class GetSingleTweet
|
||||
|
||||
|
||||
|
||||
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.set_following(following)
|
||||
|
||||
### End class GetFollowing
|
||||
|
||||
|
||||
|
||||
class GetVerified(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)
|
||||
verified = user.verified
|
||||
except (HTTPError, URLError):
|
||||
verified = false
|
||||
|
||||
self.pane.set_verified(verified)
|
||||
|
||||
### End class GetVerified
|
||||
|
||||
|
||||
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 SigProxy(gtk.Alignment):
|
||||
|
||||
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))
|
||||
|
||||
|
||||
# We use these classes to emulate a Status object when we need
|
||||
# one to be built out of something else.
|
||||
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
|
||||
|
||||
|
||||
class User():
|
||||
def __init__(self):
|
||||
self.screen_name = None
|
||||
self.name = None
|
||||
|
||||
|
||||
# To keep things simple elsewhere and improve code reuse
|
||||
# we'll build a list of home-cooked Status objects out of results.
|
||||
# Why is this even necessary?
|
||||
# Why can't we have more consistency out of the Twitter API?
|
||||
def results_to_statuses(results):
|
||||
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.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
|
||||
statuses.append(status)
|
||||
return statuses
|
||||
|
||||
|
||||
def dms_to_statuses(direct_messages):
|
||||
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.created_at = dm.created_at
|
||||
status.text = dm.text
|
||||
statuses.append(status)
|
||||
return statuses
|
|
@ -221,7 +221,7 @@
|
|||
<property name="use_underline">True</property>
|
||||
<property name="relief">GTK_RELIEF_NORMAL</property>
|
||||
<property name="focus_on_click">True</property>
|
||||
<signal name="clicked" handler="update_windows_callback" last_modification_time="Mon, 12 Apr 2010 18:33:51 GMT"/>
|
||||
<signal name="clicked" handler="update_window_callback" last_modification_time="Thu, 06 May 2010 15:25:15 GMT"/>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
|
|
312
mytwitter.py
312
mytwitter.py
|
@ -3,10 +3,10 @@
|
|||
# Custom twitter client... mostly for learning Python
|
||||
|
||||
import sys, ConfigParser, os, re, optparse, shelve
|
||||
import twitter
|
||||
import gtk, gtk.glade, gobject
|
||||
from urllib2 import HTTPError
|
||||
from urllib2 import HTTPError,URLError
|
||||
from twitterwidgets import TweetPane
|
||||
import apithreads
|
||||
|
||||
|
||||
class MyTwitter():
|
||||
|
@ -42,11 +42,16 @@ class MyTwitter():
|
|||
print "Error: You must define at least one [account] section in " + config_file
|
||||
sys.exit(1)
|
||||
|
||||
# Init the glade stuff here, so we don't have a race condition with
|
||||
# the lists-ready signal
|
||||
self.init_user_interface('./default.glade')
|
||||
|
||||
self.accounts = {}
|
||||
for item in config.sections():
|
||||
if (re.match(r'account', item)):
|
||||
username = config.get(item, 'username')
|
||||
self.accounts[username] = twitter.Api(username=username, password=config.get(item, 'password'))
|
||||
self.accounts[username] = apithreads.CustomApi(username=username, password=config.get(item, 'password'))
|
||||
self.accounts[username].sig_proxy.connect('lists-ready', self.on_lists_ready)
|
||||
|
||||
self.username = self.accounts.keys()[0]
|
||||
self.api = self.accounts[self.username]
|
||||
|
@ -68,8 +73,7 @@ class MyTwitter():
|
|||
|
||||
self.reply_id = None
|
||||
|
||||
# Load up all the GUI stuff
|
||||
self.init_user_interface('./default.glade')
|
||||
# Load up all the programmatic GUI stuff
|
||||
self.init_widgets()
|
||||
|
||||
|
||||
|
@ -77,8 +81,6 @@ class MyTwitter():
|
|||
self.widget_tree=gtk.glade.XML(path_to_skin, "window")
|
||||
self.widget_tree.signal_autoconnect(self)
|
||||
|
||||
|
||||
def init_widgets(self):
|
||||
# Get widgets from glade
|
||||
self.tweet_notebook = self.widget_tree.get_widget('tweet_notebook')
|
||||
self.view_menu = self.widget_tree.get_widget('view_menu')
|
||||
|
@ -91,6 +93,8 @@ class MyTwitter():
|
|||
self.verified_label = self.widget_tree.get_widget('verified_label')
|
||||
self.account_select = self.widget_tree.get_widget('account_select')
|
||||
|
||||
|
||||
def init_widgets(self):
|
||||
self.context_id = self.status_bar.get_context_id('message')
|
||||
|
||||
# Manual tweaks to the glade UI, to overcome its limitations
|
||||
|
@ -105,93 +109,82 @@ class MyTwitter():
|
|||
# Add the tabs from last session to the notebook
|
||||
page_num = self.db['active_page']
|
||||
for tab, single_tweet in self.db['open_tabs']:
|
||||
self.add_to_notebook(tab, single_tweet)
|
||||
self.add_to_notebook(tab, single_tweet, update=False)
|
||||
self.tweet_notebook.set_current_page(page_num)
|
||||
|
||||
# Put Home, @user, Direct Messages, and lists in the View menu for
|
||||
# each user
|
||||
for username in self.accounts.keys():
|
||||
outer_menu_item = gtk.MenuItem(username)
|
||||
self.view_menu.append(outer_menu_item)
|
||||
new_menu = gtk.Menu()
|
||||
outer_menu_item.set_submenu(new_menu)
|
||||
|
||||
lists = self.accounts[username].GetUserLists()
|
||||
list_names = []
|
||||
for l in lists['lists']:
|
||||
list_names.append(l.name)
|
||||
list_names.sort()
|
||||
list_names.insert(0, 'Home')
|
||||
list_names.insert(1, '@' + username)
|
||||
list_names.insert(2, 'Direct Messages')
|
||||
for l in list_names:
|
||||
menu_item = gtk.MenuItem(l)
|
||||
new_menu.append(menu_item)
|
||||
menu_item.connect('activate', self.on_view_selected, username, l)
|
||||
menu_item.show()
|
||||
outer_menu_item.show()
|
||||
self.update_windows()
|
||||
|
||||
# Timer to update periodically
|
||||
gobject.timeout_add(self.refresh_time * 1000, self.update_windows)
|
||||
|
||||
|
||||
# Spawns a thread for each pane, which updates that pane.
|
||||
def update_windows(self):
|
||||
for i in range(0, self.tweet_notebook.get_n_pages()):
|
||||
pane = self.tweet_notebook.get_nth_page(i)
|
||||
list_name = pane.get_list_name()
|
||||
|
||||
# Single tweets should never be updated here
|
||||
if pane.get_single_tweet() is not None:
|
||||
continue
|
||||
|
||||
# username/Home entries need to load the appropriate Home feed
|
||||
elif re.search(r'/Home', list_name):
|
||||
account = self.accounts[re.sub(r'/Home', r'', list_name)]
|
||||
statuses = account.GetHomeTimeline(count=self.num_entries)
|
||||
|
||||
# For @username, we need to check if it is one of our usernames, or
|
||||
# just needs to be searched on
|
||||
elif re.match('@', list_name):
|
||||
if self.accounts.has_key(re.sub('@', '', list_name)):
|
||||
account = self.accounts[re.sub(r'@', r'', list_name)]
|
||||
statuses = account.GetMentions(count=self.num_entries)
|
||||
else:
|
||||
statuses = self.results_to_statuses(self.api.Search(list_name, rpp=self.num_entries))
|
||||
|
||||
# Direct Messages should match like /Home, above
|
||||
elif re.search(r'/Direct Messages', list_name):
|
||||
account = self.accounts[re.sub(r'/Direct Messages', r'', list_name)]
|
||||
statuses = self.dms_to_statuses(account.GetDirectMessages())
|
||||
|
||||
# User lookups go straight to the user
|
||||
elif re.match(r'user: ', list_name):
|
||||
statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', list_name), count=self.num_entries)
|
||||
|
||||
# Lists load the appropriate list from the appropriate account
|
||||
elif re.match(r'list: ', list_name):
|
||||
username = re.sub(r'list: (.*)/.*', r'\1', list_name)
|
||||
real_list = re.sub(r'list: .*/(.*)', r'\1', list_name)
|
||||
account = self.accounts[username]
|
||||
statuses = account.GetListStatuses(real_list, per_page=self.num_entries)
|
||||
|
||||
# Everything else is a straight search
|
||||
else:
|
||||
statuses = self.results_to_statuses(self.api.Search(list_name, rpp=self.num_entries))
|
||||
|
||||
pane.update_window(statuses)
|
||||
self.update_single_window(pane)
|
||||
|
||||
# We have to return true, so the timeout_add event will keep happening
|
||||
return True
|
||||
|
||||
|
||||
def update_windows_callback(self, widget):
|
||||
self.update_windows()
|
||||
def update_single_window(self, pane):
|
||||
list_name = pane.get_list_name()
|
||||
|
||||
# Single tweets should never be updated here
|
||||
if pane.get_single_tweet() is not None:
|
||||
return
|
||||
|
||||
# Determine username and appropriate account to use
|
||||
found = False
|
||||
|
||||
username = re.sub('/Home', '', list_name)
|
||||
if self.accounts.has_key(username):
|
||||
account = self.accounts[username]
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
username = re.sub('@', '', list_name)
|
||||
if self.accounts.has_key(username):
|
||||
account = self.accounts[username]
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
username = re.sub('/Direct Messages', '', list_name)
|
||||
if self.accounts.has_key(username):
|
||||
account = self.accounts[username]
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
account = self.api
|
||||
username = self.username
|
||||
|
||||
apithreads.GetTweets(api=account,
|
||||
list_name=list_name,
|
||||
pane=pane,
|
||||
num_entries=self.num_entries,
|
||||
username=username
|
||||
).start()
|
||||
|
||||
|
||||
def update_window_callback(self, widget):
|
||||
pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page())
|
||||
self.update_single_window(pane)
|
||||
|
||||
|
||||
def update_status(self):
|
||||
reply_id = self.reply_id
|
||||
text = self.update_entry.get_text()
|
||||
self.update_entry.set_text("")
|
||||
self.api.PostUpdate(text, in_reply_to_status_id=self.reply_id)
|
||||
|
||||
with self.api.lock:
|
||||
try:
|
||||
self.api.PostUpdate(text, in_reply_to_status_id=reply_id)
|
||||
except HTTPError,URLError:
|
||||
self.update_status_bar('Failed to post tweet')
|
||||
self.reply_id = None
|
||||
return
|
||||
|
||||
self.reply_id = None
|
||||
self.update_status_bar('Tweet Posted')
|
||||
|
||||
|
@ -207,7 +200,7 @@ class MyTwitter():
|
|||
self.update_count.set_label(new_count)
|
||||
|
||||
# If reply_id is set, unset it if we have removed the @ symbol
|
||||
if self.reply_id is not None and not re.match('@', self.update_entry.get_text()):
|
||||
if self.reply_id and not re.match('@', self.update_entry.get_text()):
|
||||
self.reply_id = None
|
||||
|
||||
|
||||
|
@ -227,7 +220,11 @@ class MyTwitter():
|
|||
|
||||
|
||||
def on_retweet(self, widget, data):
|
||||
self.api.PostRetweet(data['id'])
|
||||
with self.api.lock:
|
||||
try:
|
||||
self.api.PostRetweet(data['id'])
|
||||
except HTTPError,URLError:
|
||||
self.update_status_bar('Failed to retweet')
|
||||
|
||||
|
||||
def on_reply_to(self, widget, data):
|
||||
|
@ -265,7 +262,7 @@ class MyTwitter():
|
|||
self.remove_view(name, single_tweet)
|
||||
|
||||
|
||||
def add_to_notebook(self, name, single_tweet=None):
|
||||
def add_to_notebook(self, name, single_tweet=None, update=True):
|
||||
# If it already exists, don't add it, just switch to it
|
||||
for i in range(self.tweet_notebook.get_n_pages()):
|
||||
pane = self.tweet_notebook.get_nth_page(i)
|
||||
|
@ -280,21 +277,21 @@ class MyTwitter():
|
|||
self.tweet_notebook.set_current_page(i)
|
||||
return
|
||||
|
||||
# We check for the name so that the special case of
|
||||
# the first run is handled...
|
||||
# Add the pane to the persistent database of open panes
|
||||
if (name, single_tweet) not in self.db['open_tabs']:
|
||||
ot = self.db['open_tabs']
|
||||
ot.append((name,single_tweet))
|
||||
self.db['open_tabs'] = ot
|
||||
|
||||
is_user = False
|
||||
following = False
|
||||
verified = False
|
||||
if re.match('user:', name):
|
||||
is_user = True
|
||||
following = self.check_following(name)
|
||||
verified = self.check_verified(name)
|
||||
new_pane = TweetPane(name, num_entries=self.num_entries, single_tweet=single_tweet, is_user=is_user, following=following, verified=verified)
|
||||
new_pane = TweetPane(name, num_entries=self.num_entries, single_tweet=single_tweet, is_user=is_user)
|
||||
|
||||
if is_user:
|
||||
apithreads.GetFollowing(api=self.api, pane=new_pane, user=name).start()
|
||||
apithreads.GetVerified(api=self.api, pane=new_pane, user=name).start()
|
||||
|
||||
self.tweet_notebook.append_page_menu(new_pane, new_pane.get_tab_label(), gtk.Label(name))
|
||||
self.tweet_notebook.set_tab_reorderable(new_pane, True)
|
||||
new_pane.get_tab_label().connect('close-clicked', self.remove_view_callback, name, single_tweet)
|
||||
|
@ -302,24 +299,25 @@ class MyTwitter():
|
|||
new_pane.connect('tweet-retweet', self.on_retweet)
|
||||
new_pane.connect('tweet-in-reply-to', self.on_reply_to)
|
||||
new_pane.connect('show-user', self.show_user_callback)
|
||||
new_pane.connect('following-set', self.update_follow_button)
|
||||
new_pane.connect('verified-set', self.update_verified_label)
|
||||
|
||||
# Special logic for single tweet pane
|
||||
if single_tweet is not None:
|
||||
try:
|
||||
statuses = []
|
||||
statuses.append(self.api.GetStatus(single_tweet))
|
||||
new_pane.update_window(statuses)
|
||||
except HTTPError:
|
||||
self.tweet_notebook.remove_page(-1)
|
||||
self.update_status_bar('Error retrieving tweet id: ' + str(single_tweet))
|
||||
return
|
||||
apithreads.GetSingleTweet(api=self.api,
|
||||
pane=new_pane,
|
||||
single_tweet=single_tweet).start()
|
||||
|
||||
self.update_windows()
|
||||
self.tweet_notebook.set_current_page(-1) # switch to the new pane
|
||||
if update:
|
||||
self.update_windows()
|
||||
self.tweet_notebook.set_current_page(-1) # switch to the new pane
|
||||
|
||||
|
||||
def on_tab_change(self, event, page, page_num):
|
||||
last_page = self.db['active_page']
|
||||
self.db['active_page'] = page_num
|
||||
|
||||
# Now get the new page, and set everything up
|
||||
pane = self.tweet_notebook.get_nth_page(page_num)
|
||||
pane.set_tweets_read()
|
||||
self.update_follow_button(pane)
|
||||
|
@ -327,10 +325,8 @@ class MyTwitter():
|
|||
self.at_button.show()
|
||||
else:
|
||||
self.at_button.hide()
|
||||
if pane.get_verified():
|
||||
self.verified_label.show()
|
||||
else:
|
||||
self.verified_label.hide()
|
||||
|
||||
self.update_verified_label(pane)
|
||||
|
||||
|
||||
def on_tabs_reordered(self, widget, child, page_num):
|
||||
|
@ -362,35 +358,26 @@ class MyTwitter():
|
|||
current_pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page())
|
||||
user_name = re.sub('^user: ', '', current_pane.get_list_name())
|
||||
if current_pane.get_following():
|
||||
self.api.DestroyFriendship(user_name)
|
||||
current_pane.set_following(self.check_following(user_name))
|
||||
with self.api.lock:
|
||||
try:
|
||||
self.api.DestroyFriendship(user_name)
|
||||
except HTTPError,URLError:
|
||||
self.update_status_bar('Failed to unfollow user.')
|
||||
return
|
||||
current_pane.set_following(False)
|
||||
else:
|
||||
self.api.CreateFriendship(user_name)
|
||||
current_pane.set_following(self.check_following(user_name))
|
||||
with self.api.lock:
|
||||
try:
|
||||
self.api.CreateFriendship(user_name)
|
||||
except HTTPError,URLError:
|
||||
self.update_status_bar('Failed to follow user.')
|
||||
return
|
||||
current_pane.set_following(True)
|
||||
|
||||
self.update_follow_button(current_pane)
|
||||
|
||||
|
||||
# Name is the name of a pane, with the 'user: ' in place
|
||||
def check_following(self, name):
|
||||
screen_name = re.sub('user: ', '', name)
|
||||
try:
|
||||
relationship = self.api.ShowFriendships(target_screen_name=screen_name)
|
||||
except HTTPError:
|
||||
return False
|
||||
return relationship.source.following
|
||||
|
||||
|
||||
# Name is the name of a pane, with the 'user: ' in place
|
||||
def check_verified(self, name):
|
||||
screen_name = re.sub('user: ', '', name)
|
||||
try:
|
||||
user = self.api.GetUser(screen_name)
|
||||
except HTTPError:
|
||||
return False
|
||||
return user.verified
|
||||
|
||||
|
||||
# pane should be the currently active pane
|
||||
def update_follow_button(self, pane):
|
||||
if not pane.get_is_user():
|
||||
self.following_button.set_label('')
|
||||
|
@ -403,6 +390,13 @@ class MyTwitter():
|
|||
self.following_button.show()
|
||||
|
||||
|
||||
def update_verified_label(self, pane):
|
||||
if pane.get_verified():
|
||||
self.verified_label.show()
|
||||
else:
|
||||
self.verified_label.hide()
|
||||
|
||||
|
||||
def show_user(self, name):
|
||||
self.add_to_notebook('user: ' + name)
|
||||
|
||||
|
@ -441,68 +435,38 @@ class MyTwitter():
|
|||
self.api = self.accounts[self.username]
|
||||
|
||||
|
||||
# To keep things simple elsewhere and improve code reuse
|
||||
# we'll build a list of home-cooked Status objects out of results.
|
||||
# Why is this even necessary?
|
||||
# Why can't we have more consistency out of the Twitter API?
|
||||
def results_to_statuses(self, results):
|
||||
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.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
|
||||
statuses.append(status)
|
||||
return statuses
|
||||
def on_lists_ready(self, widget, username, list_names):
|
||||
# Setup the new sub-menu
|
||||
outer_menu_item = gtk.MenuItem(username)
|
||||
self.view_menu.append(outer_menu_item)
|
||||
new_menu = gtk.Menu()
|
||||
outer_menu_item.set_submenu(new_menu)
|
||||
|
||||
# Insert the default list items
|
||||
list_names.insert(0, 'Home')
|
||||
list_names.insert(1, '@' + username)
|
||||
list_names.insert(2, 'Direct Messages')
|
||||
|
||||
def dms_to_statuses(self, direct_messages):
|
||||
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.created_at = dm.created_at
|
||||
status.text = dm.text
|
||||
statuses.append(status)
|
||||
return statuses
|
||||
# Add the items to the submenu, connect handler
|
||||
for l in list_names:
|
||||
menu_item = gtk.MenuItem(l)
|
||||
new_menu.append(menu_item)
|
||||
menu_item.connect('activate', self.on_view_selected, username, l)
|
||||
menu_item.show()
|
||||
|
||||
outer_menu_item.show()
|
||||
|
||||
### end class MyTwitter
|
||||
|
||||
|
||||
# We use these classes to emulate a Status object when we need
|
||||
# one to be built out of something else.
|
||||
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
|
||||
|
||||
class User():
|
||||
def __init__(self):
|
||||
self.screen_name = None
|
||||
self.name = None
|
||||
|
||||
|
||||
# main
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option('-c' ,'--config', dest="filename", default="~/.mytwitter.conf")
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
my_twitter = MyTwitter(options.filename)
|
||||
|
||||
gtk.gdk.threads_init()
|
||||
gtk.gdk.threads_enter()
|
||||
gtk.main()
|
||||
gtk.gdk.threads_leave()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import re
|
||||
import datetime, dateutil.tz
|
||||
import gtk, gobject
|
||||
from threading import RLock
|
||||
|
||||
|
||||
class TweetPane(gtk.ScrolledWindow):
|
||||
'''
|
||||
|
@ -13,9 +15,11 @@ class TweetPane(gtk.ScrolledWindow):
|
|||
It also gets some data from its parent, including num_entries
|
||||
'''
|
||||
|
||||
def __init__(self, list_name, num_entries=20, single_tweet=None, is_user=False, following=False, verified=False):
|
||||
def __init__(self, list_name, num_entries=20, single_tweet=None, is_user=False):
|
||||
gtk.ScrolledWindow.__init__(self)
|
||||
|
||||
self.data_lock = RLock()
|
||||
|
||||
self.updated_once = False
|
||||
|
||||
self.list_name = list_name
|
||||
|
@ -26,11 +30,13 @@ class TweetPane(gtk.ScrolledWindow):
|
|||
self.num_entries = 1
|
||||
|
||||
self.is_user = is_user
|
||||
self.following = following
|
||||
self.verified = verified
|
||||
self.following = False
|
||||
self.verified = False
|
||||
|
||||
self.tab_label = CloseTabLabel(self.list_name)
|
||||
|
||||
self.message = gtk.Label('Loading...')
|
||||
|
||||
# These handle determining which tweets are unread
|
||||
self.last_tweet_read = None
|
||||
self.latest_tweet = None
|
||||
|
@ -48,6 +54,8 @@ class TweetPane(gtk.ScrolledWindow):
|
|||
viewport = gtk.Viewport()
|
||||
|
||||
# Build us some labels...
|
||||
tweet_box.pack_start(self.message)
|
||||
|
||||
for i in range(0, self.num_entries):
|
||||
self.tweets.append(TweetBox())
|
||||
tweet_box.pack_start(self.tweets[i], expand=False)
|
||||
|
@ -68,8 +76,20 @@ class TweetPane(gtk.ScrolledWindow):
|
|||
self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
|
||||
self.show_all()
|
||||
|
||||
for tweet in self.tweets:
|
||||
tweet.hide()
|
||||
|
||||
|
||||
def update_window(self, statuses):
|
||||
if statuses is None:
|
||||
self.message.set_label('An error occurred while fetching data')
|
||||
self.message.show()
|
||||
for i in range(0, self.num_entries):
|
||||
self.tweets[i].hide()
|
||||
return
|
||||
|
||||
self.message.hide()
|
||||
|
||||
if self.updated_once is False:
|
||||
self.updated_once = True
|
||||
|
||||
|
@ -90,8 +110,13 @@ class TweetPane(gtk.ScrolledWindow):
|
|||
self.num_new_tweets += 1
|
||||
read = False
|
||||
self.tweets[i].set_status(statuses[i], read)
|
||||
self.tweets[i].show()
|
||||
else:
|
||||
self.tweets[i].clear_status()
|
||||
self.tweets[i].hide()
|
||||
|
||||
if len(statuses) == 0:
|
||||
self.message.set_label('There is no data to display')
|
||||
|
||||
try:
|
||||
self.latest_tweet = statuses[0].id
|
||||
|
@ -152,15 +177,25 @@ class TweetPane(gtk.ScrolledWindow):
|
|||
|
||||
|
||||
def get_following(self):
|
||||
return self.following
|
||||
with self.data_lock:
|
||||
return self.following
|
||||
|
||||
|
||||
def get_verified(self):
|
||||
return self.verified
|
||||
with self.data_lock:
|
||||
return self.verified
|
||||
|
||||
|
||||
def set_following(self, following):
|
||||
self.following = following
|
||||
with self.data_lock:
|
||||
self.following = following
|
||||
self.emit("following-set")
|
||||
|
||||
|
||||
def set_verified(self, verified):
|
||||
with self.data_lock:
|
||||
self.verified = verified
|
||||
self.emit("verified-set")
|
||||
|
||||
|
||||
def get_is_user(self):
|
||||
|
@ -183,6 +218,12 @@ gobject.signal_new("tweet-in-reply-to", TweetPane,
|
|||
gobject.signal_new("show-user", TweetPane,
|
||||
gobject.SIGNAL_RUN_LAST,
|
||||
gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
|
||||
gobject.signal_new("following-set", TweetPane,
|
||||
gobject.SIGNAL_RUN_LAST,
|
||||
gobject.TYPE_NONE, ())
|
||||
gobject.signal_new("verified-set", TweetPane,
|
||||
gobject.SIGNAL_RUN_LAST,
|
||||
gobject.TYPE_NONE, ())
|
||||
|
||||
|
||||
|
||||
|
@ -291,7 +332,7 @@ class TweetBox(gtk.VBox):
|
|||
self.text.set_markup(new_text)
|
||||
|
||||
# If this is in reply to something, set appropriate label
|
||||
if self.in_reply_to_screen_name:
|
||||
if self.in_reply_to_screen_name and self.in_reply_to_id:
|
||||
self.reply_to_button.set_label('in reply to ' + self.in_reply_to_screen_name)
|
||||
|
||||
|
||||
|
|
Reference in New Issue
Block a user