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

507 lines
18 KiB
Python
Executable File

#!/usr/bin/python
#
# Custom twitter client... mostly for learning Python
import sys, ConfigParser, os, re, optparse, shelve
import gtk, gtk.glade, gobject
from urllib2 import HTTPError,URLError
from twitterwidgets import TweetPane
import apithreads
class MyTwitter():
""" Display Tweets, post to twitter """
def __init__(self, config_file, resize):
self.resize = resize
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)
# 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] = 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]
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, False), ('@' + self.username, None, False), (self.username + '/Direct Messages', None, False)]
if self.refresh_time < 10:
self.refresh_time = 10
self.reply_id = None
# Load up all the programmatic GUI stuff
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)
# Get widgets from glade
self.window = self.widget_tree.get_widget('window')
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')
def init_widgets(self):
# Set the main window size
if self.resize and self.db.has_key('width') and self.db.has_key('height'):
self.window.resize(self.db['width'], self.db['height'])
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, conversation in self.db['open_tabs']:
self.add_to_notebook(tab, single_tweet, conversation)
self.tweet_notebook.set_current_page(page_num)
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)
self.update_single_window(pane)
# We have to return true, so the timeout_add event will keep happening
return True
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()
thread = apithreads.PostUpdate(self.api, text, reply_id)
thread.sig_proxy.connect('update-posted', self.on_update_posted)
self.update_entry.set_sensitive(False)
self.update_status_bar('Posting...')
thread.start()
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 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):
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):
self.add_to_notebook(data['name'], data['id'])
def on_conversation(self, widget, data):
self.add_to_notebook(data['name'], data['id'], True)
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, conversation):
ot = self.db['open_tabs']
ot.remove((name,single_tweet,conversation))
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, conversation):
self.remove_view(name, single_tweet, conversation)
def add_to_notebook(self, name, single_tweet=None, conversation=False):
# 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
# Add the pane to the persistent database of open panes
if (name, single_tweet, conversation) not in self.db['open_tabs']:
ot = self.db['open_tabs']
ot.append((name,single_tweet,conversation))
self.db['open_tabs'] = ot
is_user = False
if re.match('user:', name):
is_user = True
entries=self.num_entries
if single_tweet and not conversation:
entries=1
new_pane = TweetPane(name, num_entries=entries, single_tweet=single_tweet, is_user=is_user, conversation=conversation)
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, conversation)
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('tweet-conversation', self.on_conversation)
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:
if conversation:
apithreads.GetConversation(api=self.api,
pane=new_pane,
root_tweet_id=single_tweet).start()
else:
apithreads.GetSingleTweet(api=self.api,
pane=new_pane,
single_tweet=single_tweet).start()
else:
self.update_single_window(new_pane)
# Switch to the new pane
self.tweet_notebook.set_current_page(-1)
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)
if pane.get_is_user():
self.at_button.show()
else:
self.at_button.hide()
self.update_verified_label(pane)
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(), pane.get_conversation()))
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():
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:
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)
# pane should be the currently active pane
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 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)
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(), current_pane.get_conversation())
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]
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')
# 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()
def on_resize(self, widget, event):
self.db['width'] = event.width
self.db['height'] = event.height
def on_update_posted(self, widget, success):
if success:
self.reply_id = None
self.update_entry.set_text("")
self.update_status_bar('Tweet Posted')
else:
self.update_status_bar('Failed to post tweet')
self.update_entry.set_sensitive(True)
### end class MyTwitter
# main
parser = optparse.OptionParser()
parser.add_option('-c' ,'--config', dest="filename", default="~/.mytwitter.conf", help="read configuration from FILENAME instead of the default ~/.mytwitter.conf")
parser.add_option('-n' ,'--no-resize', dest="resize", action='store_false', default=True, help="use the default window size instead of the size from the last session")
(options, args) = parser.parse_args()
my_twitter = MyTwitter(options.filename, options.resize)
gtk.gdk.threads_init()
gtk.gdk.threads_enter()
gtk.main()
gtk.gdk.threads_leave()