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
2010-06-29 15:14:26 -04:00

666 lines
23 KiB
Python
Executable File

#!/usr/bin/python
#
# Custom twitter client... mostly for learning Python
import os, re, shelve
import gtk, gtk.glade, gobject
from urllib2 import HTTPError,URLError
from twitterwidgets import TweetPane
from threading import enumerate,Condition
import apithreads
import config
class Hrafn():
""" Display Tweets, post to twitter """
def __init__(self, resize):
self.resize = resize
self.lists = {}
self.lists_cond = Condition()
if config.config.get('global', 'trayicon') == '1':
self.use_trayicon = True
if config.config.get('global', 'taskbar_when_minimized') == '1':
self.taskbar_min = True
else:
self.taskbar_min = False
else:
self.use_trayicon = False
self.taskbar_min = True
# Init the glade stuff here, so we don't have a race condition with
# the lists-ready signal
self.init_user_interface('./ui/default.glade')
self.first_account_item = None
# And init the DB stuff here
db_file = os.path.expanduser(config.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.config.get('global', 'entries'))
self.refresh_time = int(config.config.get('global', 'refreshtime'))
if not self.db.has_key('active_user'):
self.db['active_user'] = None
# Now set up the accounts and their corresponding APIs
self.accounts = {}
for token in self.db['tokens']:
api = apithreads.CustomApi(token, self)
self.add_account(api)
self.username = self.db['active_user']
self.minimized = False
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 config.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()
# Add a system tray icon
if self.use_trayicon:
self.tray_icon = gtk.status_icon_new_from_file('ui/icon.svg')
self.tray_icon.connect('activate', self.on_tray_icon_clicked)
self.tray_icon.connect('popup-menu', self.on_tray_icon_popup)
self.tray_menu = gtk.Menu()
quit_item = gtk.ImageMenuItem(gtk.STOCK_QUIT)
quit_item.connect('activate', self.gtk_main_quit)
self.tray_menu.append(quit_item)
quit_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)
# Timer to update periodically
gobject.timeout_add(self.refresh_time * 1000, self.update_windows)
def update_account_label(self):
if self.username is not None:
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
account = None
username = re.sub('/Home', '', list_name)
if self.accounts.has_key(username):
account = self.accounts[username]
if account is None:
username = re.sub('@', '', list_name)
if self.accounts.has_key(username):
account = self.accounts[username]
if account is None:
username = re.sub('/Direct Messages', '', list_name)
if self.accounts.has_key(username):
account = self.accounts[username]
if account is None:
username = re.sub(r'list: (.*)/.*', r'\1', list_name)
if self.accounts.has_key(username):
account = self.accounts[username]
if account is None:
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: Hrafn.on_about()"
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, username=self.username, num_entries=entries, single_tweet=single_tweet, is_user=is_user, conversation=conversation)
new_pane.connect('new-tweets', self.on_read_tweets_changed)
new_pane.connect('tweets-read', self.on_read_tweets_changed)
if is_user:
# Find the lists this user is currently in, and pass those
# to the pane
found_lists = []
username = re.sub('user: ', '', name)
self.lists_cond.acquire()
while not self.lists.has_key(self.username):
self.lists_cond.wait()
for list_name in self.lists[self.username].keys():
try:
i = self.lists[self.username][list_name].index(username)
found_lists.append(list_name)
except ValueError:
pass
new_pane.set_lists(found_lists)
self.lists_cond.release()
new_pane.user_box.connect('at-clicked', self.on_at_button_clicked)
new_pane.user_box.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)
new_pane.connect('show-hashtag', self.show_hashtag)
# 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 show_hashtag(self, widget, data):
self.add_to_notebook('#' + 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'):
if self.tweet_notebook.get_current_page() == 0:
self.tweet_notebook.set_current_page(-1)
else:
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'):
if self.tweet_notebook.get_current_page() == self.tweet_notebook.get_n_pages() - 1:
self.tweet_notebook.set_current_page(0)
else:
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 add_lists(self, username, list_data):
'''
This function is called by a child thread.
It takes list info from the API, stores it for later use, and uses
the data to populate the Views menu
'''
# 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)
self.lists_cond.acquire()
# Save the member info in a data structure for later usage
self.lists[username] = list_data
self.lists_cond.notify()
self.lists_cond.release()
list_names = list_data.keys()
list_names.sort()
# Insert the default list items
list_names.insert(0, 'Home')
list_names.insert(1, '@' + username)
list_names.insert(2, 'Direct Messages')
for l in list_names:
# Add the item to the submenu, connect handler
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, self)
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
# 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 not self.db.has_key('active_user'):
self.db['active_user'] = username
elif username == self.db['active_user']:
menu_item.set_active(True)
menu_item.connect('activate', self.on_account_changed, username)
self.accounts_menu.append(menu_item)
menu_item.show()
def on_tray_icon_clicked(self, event):
if self.minimized:
self.window.deiconify()
else:
self.window.iconify()
def on_tray_icon_popup(self, icon, button, activate_time):
self.tray_menu.popup(None, None, gtk.status_icon_position_menu, button, activate_time, icon)
def on_window_state_changed(self, window, event):
if event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED:
if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
self.minimized = True
if not self.taskbar_min:
self.window.set_property('skip-taskbar-hint', True)
else:
self.minimized = False
self.window.set_property('skip-taskbar-hint', False)
def on_read_tweets_changed(self, widget):
unread_tweets = 0
for i in range(self.tweet_notebook.get_n_pages()):
pane = self.tweet_notebook.get_nth_page(i)
unread_tweets += pane.num_new_tweets
if unread_tweets > 0:
self.tray_icon.set_property('blinking', True)
else:
self.tray_icon.set_property('blinking', False)
### end class Hrafn
# main
if __name__ == "__main__":
config.init()
base_icon = gtk.gdk.pixbuf_new_from_file('ui/icon.svg')
icon = base_icon.scale_simple(128, 128, gtk.gdk.INTERP_BILINEAR)
gtk.window_set_default_icon(icon)
my_twitter = Hrafn(config.options.resize)
gtk.gdk.threads_init()
gtk.gdk.threads_enter()
gtk.main()
gtk.gdk.threads_leave()