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/mytwitter.py

451 lines
16 KiB
Python
Executable File

#!/usr/bin/python
#
# 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 twitterwidgets import TweetPane
import apithreads
class MyTwitter():
""" Display Tweets, post to twitter """
def __init__(self, config_file):
config = ConfigParser.ConfigParser()
config.read(os.path.expanduser(config_file))
# Set config options to defaults, if they are not present
new_data = False
if not config.has_section('global'):
config.add_section('global')
new_data = True
if not config.has_option('global', 'entries'):
config.set('global', 'entries', '20')
new_data = True
if not config.has_option('global', 'refreshtime'):
config.set('global', 'refreshtime', '30')
new_data = True
if not config.has_option('global', 'dbfile'):
config.set('global', 'dbfile', '~/.mytwitter.db')
new_data = True
# Write out new config data, if needed
if new_data:
config_filehandle = open(os.path.expanduser(config_file), 'wb')
config.write(config_filehandle)
config_filehandle.close()
if len(config.sections()) < 2:
print "Error: You must define at least one [account] section in " + config_file
sys.exit(1)
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.username = self.accounts.keys()[0]
self.api = self.accounts[self.username]
self.num_entries = int(config.get('global', 'entries'))
self.refresh_time = int(config.get('global', 'refreshtime'))
db_file = os.path.expanduser(config.get('global', 'dbfile'))
self.db = shelve.open(db_file)
if not self.db.has_key('active_page'):
self.db['active_page'] = 0
if not self.db.has_key('open_tabs'):
self.db['open_tabs'] = [(self.username + '/Home', None), ('@' + self.username, None), (self.username + '/Direct Messages', None)]
if self.refresh_time < 10:
self.refresh_time = 10
self.reply_id = None
# Load up all the GUI stuff
self.init_user_interface('./default.glade')
self.init_widgets()
def init_user_interface(self, path_to_skin):
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')
self.update_entry = self.widget_tree.get_widget('update_entry')
self.update_count = self.widget_tree.get_widget('update_count')
self.status_bar = self.widget_tree.get_widget('status_bar')
self.search_entry = self.widget_tree.get_widget('search_entry')
self.following_button = self.widget_tree.get_widget('following_button')
self.at_button = self.widget_tree.get_widget('at_button')
self.verified_label = self.widget_tree.get_widget('verified_label')
self.account_select = self.widget_tree.get_widget('account_select')
self.context_id = self.status_bar.get_context_id('message')
# Manual tweaks to the glade UI, to overcome its limitations
self.tweet_notebook.remove_page(0)
self.account_select.remove_text(0)
# Add entries to the account select box, set the default entry
for username in self.accounts.keys():
self.account_select.append_text(username)
self.account_select.set_active(0)
# 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.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()
# 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
# 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()
# 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_status(self):
text = self.update_entry.get_text()
self.update_entry.set_text("")
self.api.PostUpdate(text, in_reply_to_status_id=self.reply_id)
self.reply_id = None
self.update_status_bar('Tweet Posted')
def update_status_callback(self, widget):
self.update_status()
def text_watcher(self, widget):
''' Watch text entered on the update_entry, update things '''
text_len = self.update_entry.get_text_length()
new_count = str(text_len) + "/140"
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()):
self.reply_id = None
def gtk_main_quit(self, widget):
self.db.close()
gtk.main_quit()
def on_about(self, widget):
print "STUB: help->about not yet implemented"
def on_reply(self, widget, data):
self.update_entry.set_text('@' + data['screen_name'] + ' ')
self.reply_id = data['id']
self.update_entry.grab_focus()
def on_retweet(self, widget, data):
self.api.PostRetweet(data['id'])
def on_reply_to(self, widget, data):
self.add_to_notebook(data['name'], data['id'])
def on_view_selected(self, event, username, name):
if name == 'Home' or name == 'Direct Messages':
full_name = username + '/' + name
elif name == '@' + username:
full_name = name
else:
full_name = 'list: ' + username + '/' + name
# Now, add a new tab with this list
self.add_to_notebook(full_name)
# Remove one of the views from the tweet notebook.
# Called when the close button is clicked on one of the views
# or Ctrl + W is pressed while the view is active
def remove_view(self, name, single_tweet):
ot = self.db['open_tabs']
ot.remove((name,single_tweet))
self.db['open_tabs'] = ot
for i in range(self.tweet_notebook.get_n_pages()):
pane = self.tweet_notebook.get_nth_page(i)
if (pane.get_list_name() == name):
self.tweet_notebook.remove_page(i)
return
def remove_view_callback(self, event, name, single_tweet):
self.remove_view(name, single_tweet)
def add_to_notebook(self, name, single_tweet=None):
# 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)
# Unless it is a single tweet... ignore those unless
# we are also a single tweet... then, special logic
if pane.get_single_tweet() is not None:
if pane.get_single_tweet() == single_tweet:
self.tweet_notebook.set_current_page(i)
return
elif pane.get_list_name() == name:
self.tweet_notebook.set_current_page(i)
return
# We check for the name so that the special case of
# the first run is handled...
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)
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)
new_pane.connect('tweet-reply', self.on_reply)
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)
# 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
self.update_windows()
self.tweet_notebook.set_current_page(-1) # switch to the new pane
def on_tab_change(self, event, page, page_num):
self.db['active_page'] = page_num
pane = self.tweet_notebook.get_nth_page(page_num)
pane.set_tweets_read()
self.update_follow_button(pane)
if pane.get_is_user():
self.at_button.show()
else:
self.at_button.hide()
if pane.get_verified():
self.verified_label.show()
else:
self.verified_label.hide()
def on_tabs_reordered(self, widget, child, page_num):
self.db['active_page'] = page_num
# Clear the persistent tabs list, and recreate it
# from scratch
tab_names = []
for i in range(self.tweet_notebook.get_n_pages()):
pane = self.tweet_notebook.get_nth_page(i)
tab_names.append((pane.get_list_name(), pane.get_single_tweet()))
self.db['open_tabs'] = tab_names
def on_search(self, event):
search_string = self.search_entry.get_text()
self.search_entry.set_text('')
self.add_to_notebook(search_string)
def update_status_bar(self, text):
self.status_bar.pop(self.context_id)
self.status_bar.push(self.context_id, text)
def on_following_button_clicked(self, event):
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))
else:
self.api.CreateFriendship(user_name)
current_pane.set_following(self.check_following(user_name))
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
def update_follow_button(self, pane):
if not pane.get_is_user():
self.following_button.set_label('')
self.following_button.hide()
elif pane.get_following():
self.following_button.set_label('Unfollow')
self.following_button.show()
else:
self.following_button.set_label('Follow')
self.following_button.show()
def show_user(self, name):
self.add_to_notebook('user: ' + name)
def show_user_callback(self, widget, data):
self.show_user(data)
def on_at_button_clicked(self, widget):
current_pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page())
user_name = re.sub('^user: ', '', current_pane.get_list_name())
self.add_to_notebook('@' + user_name)
def global_key_press_handler(self, widget, event):
keyname = gtk.gdk.keyval_name(event.keyval)
if keyname == 'w' and event.state & gtk.gdk.CONTROL_MASK:
self.close_current_tab()
elif (keyname == 'Tab' and event.state & gtk.gdk.SHIFT_MASK and event.state & gtk.gdk.CONTROL_MASK) or (keyname == 'ISO_Left_Tab' and event.state & gtk.gdk.CONTROL_MASK) or (keyname == 'Page_Up' and event.state & gtk.gdk.CONTROL_MASK):
self.tweet_notebook.prev_page()
return True
elif (keyname == 'Tab' and event.state & gtk.gdk.CONTROL_MASK) or (keyname == 'Page_Down' and event.state & gtk.gdk.CONTROL_MASK):
self.tweet_notebook.next_page()
return True
def close_current_tab(self):
current_pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page())
self.remove_view(current_pane.get_list_name(), current_pane.get_single_tweet())
def on_account_changed(self, widget):
new_user = self.account_select.get_active_text()
if self.accounts.has_key(new_user):
self.username = new_user
self.api = self.accounts[self.username]
### end class MyTwitter
# 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.main()