#!/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()