#!/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), ('@' + 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 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 in self.db['open_tabs']: self.add_to_notebook(tab, single_tweet) 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() self.update_entry.set_text("") 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.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 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_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 # Add the pane to the persistent database of open panes 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 if re.match('user:', name): is_user = True new_pane = TweetPane(name, num_entries=self.num_entries, single_tweet=single_tweet, is_user=is_user) 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) 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) 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: apithreads.GetSingleTweet(api=self.api, pane=new_pane, single_tweet=single_tweet).start() else: self.update_single_window(new_pane) 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())) 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()) 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 ### 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()