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
|
* Status bar icon
|
||||||
* Support viewing a list of friends and unfollowing them
|
* 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)
|
* 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:
|
bugs:
|
||||||
|
|
||||||
* Direct Messages have no names, only screen names (may not be fixable without
|
* Direct Messages have no names, only screen names (may not be fixable without
|
||||||
considerable tweets to python-twitter)
|
considerable tweets to python-twitter)
|
||||||
* Apparently, reply button isn't functioning, and some posts look like they are
|
* Can't always kill tabs - open tab list not always correctly populated?
|
||||||
in reply to someone when the twitter website says they are not
|
* 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="use_underline">True</property>
|
||||||
<property name="relief">GTK_RELIEF_NORMAL</property>
|
<property name="relief">GTK_RELIEF_NORMAL</property>
|
||||||
<property name="focus_on_click">True</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>
|
</widget>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="padding">0</property>
|
<property name="padding">0</property>
|
||||||
|
|
302
mytwitter.py
302
mytwitter.py
|
@ -3,10 +3,10 @@
|
||||||
# Custom twitter client... mostly for learning Python
|
# Custom twitter client... mostly for learning Python
|
||||||
|
|
||||||
import sys, ConfigParser, os, re, optparse, shelve
|
import sys, ConfigParser, os, re, optparse, shelve
|
||||||
import twitter
|
|
||||||
import gtk, gtk.glade, gobject
|
import gtk, gtk.glade, gobject
|
||||||
from urllib2 import HTTPError
|
from urllib2 import HTTPError,URLError
|
||||||
from twitterwidgets import TweetPane
|
from twitterwidgets import TweetPane
|
||||||
|
import apithreads
|
||||||
|
|
||||||
|
|
||||||
class MyTwitter():
|
class MyTwitter():
|
||||||
|
@ -42,11 +42,16 @@ class MyTwitter():
|
||||||
print "Error: You must define at least one [account] section in " + config_file
|
print "Error: You must define at least one [account] section in " + config_file
|
||||||
sys.exit(1)
|
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 = {}
|
self.accounts = {}
|
||||||
for item in config.sections():
|
for item in config.sections():
|
||||||
if (re.match(r'account', item)):
|
if (re.match(r'account', item)):
|
||||||
username = config.get(item, 'username')
|
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.username = self.accounts.keys()[0]
|
||||||
self.api = self.accounts[self.username]
|
self.api = self.accounts[self.username]
|
||||||
|
@ -68,8 +73,7 @@ class MyTwitter():
|
||||||
|
|
||||||
self.reply_id = None
|
self.reply_id = None
|
||||||
|
|
||||||
# Load up all the GUI stuff
|
# Load up all the programmatic GUI stuff
|
||||||
self.init_user_interface('./default.glade')
|
|
||||||
self.init_widgets()
|
self.init_widgets()
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,8 +81,6 @@ class MyTwitter():
|
||||||
self.widget_tree=gtk.glade.XML(path_to_skin, "window")
|
self.widget_tree=gtk.glade.XML(path_to_skin, "window")
|
||||||
self.widget_tree.signal_autoconnect(self)
|
self.widget_tree.signal_autoconnect(self)
|
||||||
|
|
||||||
|
|
||||||
def init_widgets(self):
|
|
||||||
# Get widgets from glade
|
# Get widgets from glade
|
||||||
self.tweet_notebook = self.widget_tree.get_widget('tweet_notebook')
|
self.tweet_notebook = self.widget_tree.get_widget('tweet_notebook')
|
||||||
self.view_menu = self.widget_tree.get_widget('view_menu')
|
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.verified_label = self.widget_tree.get_widget('verified_label')
|
||||||
self.account_select = self.widget_tree.get_widget('account_select')
|
self.account_select = self.widget_tree.get_widget('account_select')
|
||||||
|
|
||||||
|
|
||||||
|
def init_widgets(self):
|
||||||
self.context_id = self.status_bar.get_context_id('message')
|
self.context_id = self.status_bar.get_context_id('message')
|
||||||
|
|
||||||
# Manual tweaks to the glade UI, to overcome its limitations
|
# 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
|
# Add the tabs from last session to the notebook
|
||||||
page_num = self.db['active_page']
|
page_num = self.db['active_page']
|
||||||
for tab, single_tweet in self.db['open_tabs']:
|
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)
|
self.tweet_notebook.set_current_page(page_num)
|
||||||
|
|
||||||
# Put Home, @user, Direct Messages, and lists in the View menu for
|
self.update_windows()
|
||||||
# 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()
|
|
||||||
|
|
||||||
# Timer to update periodically
|
# Timer to update periodically
|
||||||
gobject.timeout_add(self.refresh_time * 1000, self.update_windows)
|
gobject.timeout_add(self.refresh_time * 1000, self.update_windows)
|
||||||
|
|
||||||
|
|
||||||
|
# Spawns a thread for each pane, which updates that pane.
|
||||||
def update_windows(self):
|
def update_windows(self):
|
||||||
for i in range(0, self.tweet_notebook.get_n_pages()):
|
for i in range(0, self.tweet_notebook.get_n_pages()):
|
||||||
pane = self.tweet_notebook.get_nth_page(i)
|
pane = self.tweet_notebook.get_nth_page(i)
|
||||||
list_name = pane.get_list_name()
|
self.update_single_window(pane)
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
# We have to return true, so the timeout_add event will keep happening
|
# We have to return true, so the timeout_add event will keep happening
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def update_windows_callback(self, widget):
|
def update_single_window(self, pane):
|
||||||
self.update_windows()
|
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):
|
def update_status(self):
|
||||||
|
reply_id = self.reply_id
|
||||||
text = self.update_entry.get_text()
|
text = self.update_entry.get_text()
|
||||||
self.update_entry.set_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.reply_id = None
|
||||||
self.update_status_bar('Tweet Posted')
|
self.update_status_bar('Tweet Posted')
|
||||||
|
|
||||||
|
@ -207,7 +200,7 @@ class MyTwitter():
|
||||||
self.update_count.set_label(new_count)
|
self.update_count.set_label(new_count)
|
||||||
|
|
||||||
# If reply_id is set, unset it if we have removed the @ symbol
|
# 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
|
self.reply_id = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -227,7 +220,11 @@ class MyTwitter():
|
||||||
|
|
||||||
|
|
||||||
def on_retweet(self, widget, data):
|
def on_retweet(self, widget, data):
|
||||||
|
with self.api.lock:
|
||||||
|
try:
|
||||||
self.api.PostRetweet(data['id'])
|
self.api.PostRetweet(data['id'])
|
||||||
|
except HTTPError,URLError:
|
||||||
|
self.update_status_bar('Failed to retweet')
|
||||||
|
|
||||||
|
|
||||||
def on_reply_to(self, widget, data):
|
def on_reply_to(self, widget, data):
|
||||||
|
@ -265,7 +262,7 @@ class MyTwitter():
|
||||||
self.remove_view(name, single_tweet)
|
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
|
# If it already exists, don't add it, just switch to it
|
||||||
for i in range(self.tweet_notebook.get_n_pages()):
|
for i in range(self.tweet_notebook.get_n_pages()):
|
||||||
pane = self.tweet_notebook.get_nth_page(i)
|
pane = self.tweet_notebook.get_nth_page(i)
|
||||||
|
@ -280,21 +277,21 @@ class MyTwitter():
|
||||||
self.tweet_notebook.set_current_page(i)
|
self.tweet_notebook.set_current_page(i)
|
||||||
return
|
return
|
||||||
|
|
||||||
# We check for the name so that the special case of
|
# Add the pane to the persistent database of open panes
|
||||||
# the first run is handled...
|
|
||||||
if (name, single_tweet) not in self.db['open_tabs']:
|
if (name, single_tweet) not in self.db['open_tabs']:
|
||||||
ot = self.db['open_tabs']
|
ot = self.db['open_tabs']
|
||||||
ot.append((name,single_tweet))
|
ot.append((name,single_tweet))
|
||||||
self.db['open_tabs'] = ot
|
self.db['open_tabs'] = ot
|
||||||
|
|
||||||
is_user = False
|
is_user = False
|
||||||
following = False
|
|
||||||
verified = False
|
|
||||||
if re.match('user:', name):
|
if re.match('user:', name):
|
||||||
is_user = True
|
is_user = True
|
||||||
following = self.check_following(name)
|
new_pane = TweetPane(name, num_entries=self.num_entries, single_tweet=single_tweet, is_user=is_user)
|
||||||
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)
|
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.append_page_menu(new_pane, new_pane.get_tab_label(), gtk.Label(name))
|
||||||
self.tweet_notebook.set_tab_reorderable(new_pane, True)
|
self.tweet_notebook.set_tab_reorderable(new_pane, True)
|
||||||
new_pane.get_tab_label().connect('close-clicked', self.remove_view_callback, name, single_tweet)
|
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-retweet', self.on_retweet)
|
||||||
new_pane.connect('tweet-in-reply-to', self.on_reply_to)
|
new_pane.connect('tweet-in-reply-to', self.on_reply_to)
|
||||||
new_pane.connect('show-user', self.show_user_callback)
|
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
|
# Special logic for single tweet pane
|
||||||
if single_tweet is not None:
|
if single_tweet is not None:
|
||||||
try:
|
apithreads.GetSingleTweet(api=self.api,
|
||||||
statuses = []
|
pane=new_pane,
|
||||||
statuses.append(self.api.GetStatus(single_tweet))
|
single_tweet=single_tweet).start()
|
||||||
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
|
|
||||||
|
|
||||||
|
if update:
|
||||||
self.update_windows()
|
self.update_windows()
|
||||||
self.tweet_notebook.set_current_page(-1) # switch to the new pane
|
self.tweet_notebook.set_current_page(-1) # switch to the new pane
|
||||||
|
|
||||||
|
|
||||||
def on_tab_change(self, event, page, page_num):
|
def on_tab_change(self, event, page, page_num):
|
||||||
|
last_page = self.db['active_page']
|
||||||
self.db['active_page'] = page_num
|
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 = self.tweet_notebook.get_nth_page(page_num)
|
||||||
pane.set_tweets_read()
|
pane.set_tweets_read()
|
||||||
self.update_follow_button(pane)
|
self.update_follow_button(pane)
|
||||||
|
@ -327,10 +325,8 @@ class MyTwitter():
|
||||||
self.at_button.show()
|
self.at_button.show()
|
||||||
else:
|
else:
|
||||||
self.at_button.hide()
|
self.at_button.hide()
|
||||||
if pane.get_verified():
|
|
||||||
self.verified_label.show()
|
self.update_verified_label(pane)
|
||||||
else:
|
|
||||||
self.verified_label.hide()
|
|
||||||
|
|
||||||
|
|
||||||
def on_tabs_reordered(self, widget, child, page_num):
|
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())
|
current_pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page())
|
||||||
user_name = re.sub('^user: ', '', current_pane.get_list_name())
|
user_name = re.sub('^user: ', '', current_pane.get_list_name())
|
||||||
if current_pane.get_following():
|
if current_pane.get_following():
|
||||||
|
with self.api.lock:
|
||||||
|
try:
|
||||||
self.api.DestroyFriendship(user_name)
|
self.api.DestroyFriendship(user_name)
|
||||||
current_pane.set_following(self.check_following(user_name))
|
except HTTPError,URLError:
|
||||||
|
self.update_status_bar('Failed to unfollow user.')
|
||||||
|
return
|
||||||
|
current_pane.set_following(False)
|
||||||
else:
|
else:
|
||||||
|
with self.api.lock:
|
||||||
|
try:
|
||||||
self.api.CreateFriendship(user_name)
|
self.api.CreateFriendship(user_name)
|
||||||
current_pane.set_following(self.check_following(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)
|
self.update_follow_button(current_pane)
|
||||||
|
|
||||||
|
|
||||||
# Name is the name of a pane, with the 'user: ' in place
|
# pane should be the currently active pane
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def update_follow_button(self, pane):
|
def update_follow_button(self, pane):
|
||||||
if not pane.get_is_user():
|
if not pane.get_is_user():
|
||||||
self.following_button.set_label('')
|
self.following_button.set_label('')
|
||||||
|
@ -403,6 +390,13 @@ class MyTwitter():
|
||||||
self.following_button.show()
|
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):
|
def show_user(self, name):
|
||||||
self.add_to_notebook('user: ' + name)
|
self.add_to_notebook('user: ' + name)
|
||||||
|
|
||||||
|
@ -441,68 +435,38 @@ class MyTwitter():
|
||||||
self.api = self.accounts[self.username]
|
self.api = self.accounts[self.username]
|
||||||
|
|
||||||
|
|
||||||
# To keep things simple elsewhere and improve code reuse
|
def on_lists_ready(self, widget, username, list_names):
|
||||||
# we'll build a list of home-cooked Status objects out of results.
|
# Setup the new sub-menu
|
||||||
# Why is this even necessary?
|
outer_menu_item = gtk.MenuItem(username)
|
||||||
# Why can't we have more consistency out of the Twitter API?
|
self.view_menu.append(outer_menu_item)
|
||||||
def results_to_statuses(self, results):
|
new_menu = gtk.Menu()
|
||||||
statuses = []
|
outer_menu_item.set_submenu(new_menu)
|
||||||
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
|
|
||||||
|
|
||||||
|
# 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):
|
# Add the items to the submenu, connect handler
|
||||||
statuses = []
|
for l in list_names:
|
||||||
for dm in direct_messages:
|
menu_item = gtk.MenuItem(l)
|
||||||
status = Status()
|
new_menu.append(menu_item)
|
||||||
status.id = dm.id
|
menu_item.connect('activate', self.on_view_selected, username, l)
|
||||||
status.user = User()
|
menu_item.show()
|
||||||
status.user.screen_name = dm.sender_screen_name
|
|
||||||
status.user.name = dm.sender.name
|
outer_menu_item.show()
|
||||||
status.created_at = dm.created_at
|
|
||||||
status.text = dm.text
|
|
||||||
statuses.append(status)
|
|
||||||
return statuses
|
|
||||||
|
|
||||||
### end class MyTwitter
|
### 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
|
# main
|
||||||
parser = optparse.OptionParser()
|
parser = optparse.OptionParser()
|
||||||
parser.add_option('-c' ,'--config', dest="filename", default="~/.mytwitter.conf")
|
parser.add_option('-c' ,'--config', dest="filename", default="~/.mytwitter.conf")
|
||||||
(options, args) = parser.parse_args()
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
my_twitter = MyTwitter(options.filename)
|
my_twitter = MyTwitter(options.filename)
|
||||||
|
|
||||||
|
gtk.gdk.threads_init()
|
||||||
|
gtk.gdk.threads_enter()
|
||||||
gtk.main()
|
gtk.main()
|
||||||
|
gtk.gdk.threads_leave()
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import re
|
import re
|
||||||
import datetime, dateutil.tz
|
import datetime, dateutil.tz
|
||||||
import gtk, gobject
|
import gtk, gobject
|
||||||
|
from threading import RLock
|
||||||
|
|
||||||
|
|
||||||
class TweetPane(gtk.ScrolledWindow):
|
class TweetPane(gtk.ScrolledWindow):
|
||||||
'''
|
'''
|
||||||
|
@ -13,9 +15,11 @@ class TweetPane(gtk.ScrolledWindow):
|
||||||
It also gets some data from its parent, including num_entries
|
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)
|
gtk.ScrolledWindow.__init__(self)
|
||||||
|
|
||||||
|
self.data_lock = RLock()
|
||||||
|
|
||||||
self.updated_once = False
|
self.updated_once = False
|
||||||
|
|
||||||
self.list_name = list_name
|
self.list_name = list_name
|
||||||
|
@ -26,11 +30,13 @@ class TweetPane(gtk.ScrolledWindow):
|
||||||
self.num_entries = 1
|
self.num_entries = 1
|
||||||
|
|
||||||
self.is_user = is_user
|
self.is_user = is_user
|
||||||
self.following = following
|
self.following = False
|
||||||
self.verified = verified
|
self.verified = False
|
||||||
|
|
||||||
self.tab_label = CloseTabLabel(self.list_name)
|
self.tab_label = CloseTabLabel(self.list_name)
|
||||||
|
|
||||||
|
self.message = gtk.Label('Loading...')
|
||||||
|
|
||||||
# These handle determining which tweets are unread
|
# These handle determining which tweets are unread
|
||||||
self.last_tweet_read = None
|
self.last_tweet_read = None
|
||||||
self.latest_tweet = None
|
self.latest_tweet = None
|
||||||
|
@ -48,6 +54,8 @@ class TweetPane(gtk.ScrolledWindow):
|
||||||
viewport = gtk.Viewport()
|
viewport = gtk.Viewport()
|
||||||
|
|
||||||
# Build us some labels...
|
# Build us some labels...
|
||||||
|
tweet_box.pack_start(self.message)
|
||||||
|
|
||||||
for i in range(0, self.num_entries):
|
for i in range(0, self.num_entries):
|
||||||
self.tweets.append(TweetBox())
|
self.tweets.append(TweetBox())
|
||||||
tweet_box.pack_start(self.tweets[i], expand=False)
|
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.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
|
||||||
self.show_all()
|
self.show_all()
|
||||||
|
|
||||||
|
for tweet in self.tweets:
|
||||||
|
tweet.hide()
|
||||||
|
|
||||||
|
|
||||||
def update_window(self, statuses):
|
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:
|
if self.updated_once is False:
|
||||||
self.updated_once = True
|
self.updated_once = True
|
||||||
|
|
||||||
|
@ -90,8 +110,13 @@ class TweetPane(gtk.ScrolledWindow):
|
||||||
self.num_new_tweets += 1
|
self.num_new_tweets += 1
|
||||||
read = False
|
read = False
|
||||||
self.tweets[i].set_status(statuses[i], read)
|
self.tweets[i].set_status(statuses[i], read)
|
||||||
|
self.tweets[i].show()
|
||||||
else:
|
else:
|
||||||
self.tweets[i].clear_status()
|
self.tweets[i].clear_status()
|
||||||
|
self.tweets[i].hide()
|
||||||
|
|
||||||
|
if len(statuses) == 0:
|
||||||
|
self.message.set_label('There is no data to display')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.latest_tweet = statuses[0].id
|
self.latest_tweet = statuses[0].id
|
||||||
|
@ -152,15 +177,25 @@ class TweetPane(gtk.ScrolledWindow):
|
||||||
|
|
||||||
|
|
||||||
def get_following(self):
|
def get_following(self):
|
||||||
|
with self.data_lock:
|
||||||
return self.following
|
return self.following
|
||||||
|
|
||||||
|
|
||||||
def get_verified(self):
|
def get_verified(self):
|
||||||
|
with self.data_lock:
|
||||||
return self.verified
|
return self.verified
|
||||||
|
|
||||||
|
|
||||||
def set_following(self, following):
|
def set_following(self, following):
|
||||||
|
with self.data_lock:
|
||||||
self.following = following
|
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):
|
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_new("show-user", TweetPane,
|
||||||
gobject.SIGNAL_RUN_LAST,
|
gobject.SIGNAL_RUN_LAST,
|
||||||
gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
|
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)
|
self.text.set_markup(new_text)
|
||||||
|
|
||||||
# If this is in reply to something, set appropriate label
|
# 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)
|
self.reply_to_button.set_label('in reply to ' + self.in_reply_to_screen_name)
|
||||||
|
|
||||||
|
|
||||||
|
|
Reference in New Issue
Block a user