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

587 lines
20 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
from threading import enumerate
import apithreads
class Hrafn():
""" Display Tweets, post to twitter """
def __init__(self, config_file, resize):
global debug
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', '5')
new_data = True
if not config.has_option('global', 'dbfile'):
config.set('global', 'dbfile', '~/.hrafn.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 config.has_option('global', 'debug') and config.get('global', 'debug') == '1':
debug = True
# 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.first_account_item = None
# And init the DB stuff here
db_file = os.path.expanduser(config.get('global', 'dbfile'))
self.db = shelve.open(db_file)
if not self.db.has_key('tokens'):
self.db['tokens'] = []
if not self.db.has_key('active_page'):
self.db['active_page'] = 0
self.num_entries = int(config.get('global', 'entries'))
self.refresh_time = int(config.get('global', 'refreshtime'))
# Now set up the accounts and their corresponding APIs
self.accounts = {}
for token in self.db['tokens']:
api = apithreads.CustomApi(token)
self.add_account(api)
if not self.db.has_key('active_user'):
try:
self.db['active_user'] = self.accounts.keys()[0]
except IndexError:
self.db['active_user'] = None
self.username = self.db['active_user']
try:
self.api = self.accounts[self.username]
except KeyError:
self.api = None
if not self.db.has_key('open_tabs'):
self.db['open_tabs'] = []
# refresh_time is in minutes... convert to seconds here
self.refresh_time *= 60
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.accounts_menu = self.widget_tree.get_widget('accounts_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_label = self.widget_tree.get_widget('account_label')
self.help_menu = self.widget_tree.get_widget('help_menu')
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')
# Add debug options to help menu
if debug:
menu_item = gtk.MenuItem('debug: Show Threads')
menu_item.connect('activate', self.debug_show_threads)
self.help_menu.append(menu_item)
menu_item.show()
# Set the account label
self.update_account_label()
# Manual tweaks to the glade UI, to overcome its limitations
self.tweet_notebook.remove_page(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)
def update_account_label(self):
if self.username:
self.account_label.set_text(self.username + ': ')
# 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.get_current_pane()
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):
thread = apithreads.PostRetweet(self.api, data['id'])
thread.sig_proxy.connect('retweet-posted', self.on_retweet_posted)
self.update_entry.set_sensitive(False)
self.update_status_bar('Posting retweet...')
thread.start()
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 get_current_pane(self):
return self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page())
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:
new_pane.connect('at-clicked', self.on_at_button_clicked)
new_pane.connect('follow-clicked', self.on_follow_button_clicked)
apithreads.GetFollowing(api=self.api, pane=new_pane, user=name).start()
apithreads.GetUserInfo(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)
# 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()
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_friendship_changed(self, widget, data):
if data['success']:
if data['follow']:
self.update_status_bar('Now following ' + data['user_name'])
else:
self.update_status_bar('No longer following ' + data['user_name'])
else: # didn't succeed
if data['follow']:
self.update_status_bar('Failed to follow ' + data['user_name'])
else:
self.update_status_bar('Failed to unfollow ' + data['user_name'])
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, user_name):
self.add_to_notebook('@' + user_name)
def on_follow_button_clicked(self, widget, follow):
user_name = re.sub('^user: ', '', widget.get_list_name())
thread = apithreads.ChangeFriendship(self.api, widget, user_name, follow)
thread.sig_proxy.connect('friendship-changed', self.on_friendship_changed)
thread.start()
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()
# Ctrl + Shift + Tab or Ctrl + PgUp or Ctrl + Left should go to prev tab
elif event.state & gtk.gdk.CONTROL_MASK and ((keyname == 'Tab' and event.state & gtk.gdk.SHIFT_MASK) or keyname == 'ISO_Left_Tab' or keyname == 'Page_Up' or keyname == 'Left'):
self.tweet_notebook.prev_page()
return True
# Ctrl + Tab or Ctrl + PgDown or Ctrl + Right should go to next tab
elif event.state & gtk.gdk.CONTROL_MASK and (keyname == 'Tab' or keyname == 'Page_Down' or keyname == 'Right'):
self.tweet_notebook.next_page()
return True
else:
scrolltype = None
if keyname == 'Page_Down':
scrolltype = gtk.SCROLL_PAGE_FORWARD
elif keyname == 'Page_Up':
scrolltype = gtk.SCROLL_PAGE_BACKWARD
elif keyname == 'Up':
scrolltype = gtk.SCROLL_STEP_BACKWARD
elif keyname == 'Down':
scrolltype = gtk.SCROLL_STEP_FORWARD
if scrolltype:
self.get_current_pane().emit('scroll-child', scrolltype, False)
return True
def close_current_tab(self):
current_pane = self.get_current_pane()
self.remove_view(current_pane.get_list_name(), current_pane.get_single_tweet(), current_pane.get_conversation())
def on_account_changed(self, widget, new_account):
if not (widget.get_active() and self.accounts.has_key(new_account)):
return
self.username = new_account
self.api = self.accounts[self.username]
self.db['active_user'] = self.username
self.update_account_label()
for i in range(0, self.tweet_notebook.get_n_pages()):
pane = self.tweet_notebook.get_nth_page(i)
if re.match(r'user: ', pane.get_list_name()):
user = re.sub(r'user: ', r'', pane.get_list_name())
apithreads.GetFollowing(api=self.api, pane=pane, user=user).start()
def on_lists_ready(self, widget, username, list_names):
# Setup the new sub-menu
outer_menu_item = gtk.MenuItem(username, False)
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, False)
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)
def on_retweet_posted(self, widget, success):
if success:
self.update_status_bar('Retweet Posted')
else:
self.update_status_bar('Failed to retweet')
self.update_entry.set_sensitive(True)
def debug_show_threads(self, widget):
print 'debug_show_threads()'
for thread in enumerate():
print 'debug: thread: ' + thread.name
def on_file_add_account(self, widget):
token = apithreads.get_access_token(self.window)
if token is None:
return
api = apithreads.CustomApi(token)
if not self.accounts.has_key(api.username):
tokens = self.db['tokens']
tokens.append(token)
self.db['tokens'] = tokens
self.add_account(api)
def add_account(self, api):
username = api.username
self.accounts[username] = api
self.accounts[username].sig_proxy.connect('lists-ready', self.on_lists_ready)
# Add account's menu item
menu_item = gtk.RadioMenuItem(self.first_account_item, label=username, use_underline=False)
if not self.first_account_item:
self.first_account_item = menu_item
menu_item.set_draw_as_radio(False)
if username == self.username:
menu_item.set_active(True)
menu_item.connect('activate', self.on_account_changed, username)
self.accounts_menu.append(menu_item)
menu_item.show()
### end class Hrafn
# main
debug = False
parser = optparse.OptionParser()
parser.add_option('-c' ,'--config', dest="filename", default="~/.hrafn.conf", help="read configuration from FILENAME instead of the default ~/.hrafn.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 = Hrafn(options.filename, options.resize)
gtk.gdk.threads_init()
gtk.gdk.threads_enter()
gtk.main()
gtk.gdk.threads_leave()