#!/usr/bin/python # # Custom twitter client... mostly for learning Python import sys, twitter, ConfigParser, os, datetime, dateutil.tz, gtk, gtk.glade, gobject, re, subprocess class MyTwitter(): """ Display Tweets, post to twitter """ # Precompile a regex for searching for @ at the beginning of a string at_check = re.compile('^@') def __init__(self): config = ConfigParser.ConfigParser() config.read(os.path.expanduser("~/.mytwitter")) self.username = config.get('global', 'username') self.password = config.get('global', 'password') self.num_entries = int(config.get('global', 'entries')) self.refresh_time = int(config.get('global', 'refreshtime')) if self.refresh_time < 10: self.refresh_time = 10 self.reply_id = None # Authenticate with twitter, set up the API object self.api = twitter.Api(username=self.username, password=self.password) # 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.context_id = self.status_bar.get_context_id('message') self.tweet_notebook.remove_page(0) # kill the page that glade makes us have # When we change tabs, any unread tweets in it become read self.tweet_notebook.connect('switch_page', self.on_tab_change) # Add the Home tab to the notebook home_pane = TweetPane('Home', self) self.add_to_notebook(home_pane, 'Home') # Put Home, @user, and lists in the View menu... lists = self.api.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, '@' + self.username) for l in list_names: menu_item = gtk.MenuItem(l) self.view_menu.append(menu_item) menu_item.connect('activate', self.on_view_selected, l) menu_item.show() # Timer to update periodically self.update_windows() gobject.timeout_add(self.refresh_time * 1000, self.update_windows) def update_windows(self): print 'debug: update_windows()' 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() if list_name is None or list_name == 'Home': statuses = self.api.GetHomeTimeline(count=self.num_entries) elif list_name == '@' + self.username: statuses = self.api.GetMentions(count=self.num_entries) elif re.search('user: ', list_name): statuses = self.api.GetUserTimeline(re.sub('^user: ', '', list_name), count=self.num_entries) elif re.search('#', list_name): statuses = self.api.Search(list_name, rpp=self.num_entries) else: statuses = self.api.GetListStatuses(list_name, per_page=self.num_entries) pane.update_window(statuses) # Update the label with the number of unread tweets pane_text = list_name if pane.get_num_new_tweets() > 0: pane_text += ' (' + str(pane.get_num_new_tweets()) + ')' self.tweet_notebook.get_tab_label(pane).set_label_text(pane_text) # 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.status_bar.push(self.context_id, '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 MyTwitter.at_check.match(self.update_entry.get_text()): self.reply_id = None def gtk_main_quit(self, widget): gtk.main_quit() def on_about(self, widget): print "STUB: help->about not yet implemented" def on_reply(self, widget): self.update_entry.set_text('@' + widget.screen_name + ' ') self.reply_id = widget.id self.update_entry.grab_focus() def on_retweet(self, widget): self.api.PostRetweet(widget.id) def on_view_selected(self, event, name): # 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) if pane.get_list_name() == name: self.tweet_notebook.set_current_page(i) return # Now, add a new tab with this list new_pane = TweetPane(name, self) self.add_to_notebook(new_pane, name) # And, to propagate it: self.update_windows() # Remove one of the views from the tweet notebook. # Called when the close button is clicked on one of the views def remove_view(self, event, name): 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 # This code modified from create_custom_tab in: # http://www.daa.com.au/pipermail/pygtk/2006-April/012216.html def add_to_notebook(self, widget, name): tab_label = CloseTabLabel(name) self.tweet_notebook.append_page(widget, tab_label) tab_label.connect('clicked', self.remove_view, name) self.tweet_notebook.set_current_page(-1) # switch to the new pane def on_tab_change(self, event, page, page_num): pane = self.tweet_notebook.get_nth_page(page_num) tab_label = self.tweet_notebook.get_tab_label(pane) pane.set_tweets_read() tab_label.set_label_text(pane.get_list_name()) ### end class MyTwitter class TweetPane(gtk.ScrolledWindow): ''' Box that holds all the TweetBoxes for a given feed This box will not update itself, the parent should do that. It will connect num_entries listeners to its parent's on_reply() and on_retweet() It also gets some data from its parent, including num_entries ''' def __init__(self, list_name, mytwitter): gtk.ScrolledWindow.__init__(self) self.list_name = list_name self.mytwitter = mytwitter # These handle determining which tweets are unread self.last_tweet_read = None self.latest_tweet = None self.num_new_tweets = 0 self.tweets = [] self.init_widgets() def init_widgets(self): tweet_box = gtk.VBox() viewport = gtk.Viewport() # Build us some labels... for i in range(0, self.mytwitter.num_entries): self.tweets.append(TweetBox()) tweet_box.pack_start(self.tweets[i]) self.tweets[i].connect('reply', self.mytwitter.on_reply) self.tweets[i].connect('retweet', self.mytwitter.on_retweet) viewport.add(tweet_box) # Several different actions should mark the tweets as 'read' self.connect('focus', self.set_tweets_read_callback) viewport.connect('button_press_event', self.set_tweets_read_callback) self.connect('scroll-event', self.set_tweets_read_callback) self.connect('scroll-child', self.set_tweets_read_callback) self.add(viewport) self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) self.show_all() def update_window(self, statuses): # If this is our first load of this list, don't treat anything as new! if self.last_tweet_read is None: self.last_tweet_read = statuses[0].id # Keep count of the new tweets for posting a status message self.num_new_tweets = 0 for i in range(0, self.mytwitter.num_entries): read = True if i < len(statuses): if statuses[i].id > self.last_tweet_read: self.num_new_tweets += 1 read = False self.tweets[i].set_status(statuses[i], read) else: self.tweets[i].clear_status() self.latest_tweet = statuses[0].id def get_list_name(self): return self.list_name def set_tweets_read(self): self.last_tweet_read = self.latest_tweet self.num_new_tweets = 0 def set_tweets_read_callback(self, event, arg1=None, arg2=None): self.set_tweets_read() def get_num_new_tweets(self): return self.num_new_tweets ### end class TweetPane class TweetBox(gtk.VBox): ''' GUI for displaying one tweet and associated buttons Also stores the data necessary for replying or retweeting (id, screen name) ''' def __init__(self): gtk.VBox.__init__(self) self.screen_name = None self.id = None self.init_widgets() def init_widgets(self): ## Build the header self.header = gtk.Label() label_eb = gtk.EventBox() label_eb.add(self.header) self.pack_start(label_eb) # Set the header's properties label_eb.modify_text(gtk.STATE_NORMAL,gtk.gdk.color_parse("#ffffff")) label_eb.modify_bg(gtk.STATE_NORMAL,gtk.gdk.color_parse("#8888ff")) self.header.set_alignment(0.0, 0.0) self.header.set_selectable(True) self.header.set_line_wrap(True) ## Build the text self.text = gtk.Label() text_align = gtk.Alignment() text_align.add(self.text) self.text_eb = gtk.EventBox() self.text_eb.add(text_align) self.pack_start(self.text_eb) # Set the text's properties text_align.set_padding(2, 10, 3, 0) self.text.set_alignment(0.0, 0.0) self.text.set_selectable(True) self.text.set_line_wrap(True) if gtk.gtk_version[0] > 2 or (gtk.gtk_version[0] == 2 and gtk.gtk_version[1] >= 18): self.text.connect('activate-link', self.on_url_clicked) button_box = gtk.HBox() self.pack_start(button_box) reply_button = gtk.Button("Reply") button_box.pack_start(reply_button, expand=False) reply_button.connect("clicked", self.on_reply_clicked) retweet_button = gtk.Button("Retweet") button_box.pack_start(retweet_button, expand=False) retweet_button.connect("clicked", self.on_retweet_clicked) def set_status(self, status, read=True): self.set_read(read) timezone = dateutil.tz.gettz() time_format = "%Y.%m.%d %H:%M:%S %Z" # Get the user object user = status.user # Get user's data for retweeting / replying self.screen_name = user.screen_name self.id = status.id # ... and a formatted timestamp timestamp = datetime.datetime.strptime(status.created_at, "%a %b %d %H:%M:%S +0000 %Y") timestamp = timestamp.replace(tzinfo=dateutil.tz.gettz('UTC')) timestring = timestamp.astimezone(timezone).strftime(time_format) # Set the header self.header.set_markup(user.name + " (" + user.screen_name + ") " + timestring) # and the text new_text = status.text new_text = re.sub(r'&([^;]*?)( |$)', r'&\1\2', new_text) if gtk.gtk_version[0] > 2 or (gtk.gtk_version[0] == 2 and gtk.gtk_version[1] >= 18): new_text = re.sub(r"(http://.*?)( |$)", r'\1\2', new_text) self.text.set_markup(new_text) def clear_status(self): self.header.set_markup('') self.text.set_markup('') self.screen_name = None self.id = None self.set_read(True) def set_read(self, read=True): if read: self.text_eb.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#f2f1f0")) else: self.text_eb.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#dbffdb")) def on_reply_clicked(self, widget): self.emit('reply') def on_retweet_clicked(self, widget): self.emit('retweet') def on_url_clicked(self, widget): # fixme: for now, hard code firefox, since that's what I use # Eventually make this configgable # fixme: this doesn't work at all right now... figure out how to make this signal happen print 'debug: on_url_clicked()' # subprocess.Popen('/usr/bin/firefox ' + self.text.get_current_uri(), shell=False).pid # end class TweetBox class CloseTabLabel(gtk.EventBox): ''' This class holds a label and a button with an 'I' in it. This button causes the CloseTabLabel to emit a clicked signal ''' def __init__(self, name=None): gtk.EventBox.__init__(self) self.init_widgets(name) # This code is still heinous, but at least it is # isolated to its own class def init_widgets(self, name): #create a custom tab for notebook containing a #label and a button with STOCK_ICON tabBox = gtk.HBox(False, 2) tabButton=gtk.Button() tabButton.connect('clicked', self.on_clicked) self.label = gtk.Label(name) #Add a picture on a button iconBox = gtk.HBox(False, 0) image = gtk.Image() image.set_from_stock(gtk.STOCK_CLOSE,gtk.ICON_SIZE_MENU) gtk.Button.set_relief(tabButton,gtk.RELIEF_NONE) settings = gtk.Widget.get_settings(tabButton) (w,h) = gtk.icon_size_lookup_for_settings(settings,gtk.ICON_SIZE_MENU) gtk.Widget.set_size_request(tabButton, w + 4, h + 4); iconBox.pack_start(image, True, False, 0) tabButton.add(iconBox) tabBox.pack_start(self.label, False) tabBox.pack_start(tabButton, False) # needed, otherwise even calling show_all on the notebook won't # make the hbox contents appear. tabBox.show_all() self.add(tabBox) def set_label_text(self, new_text): self.label.set_text(new_text) def on_clicked(self, event): self.emit('clicked') ### end class CloseTabLabel # main # Create custom events for TweetBox gobject.signal_new("clicked", CloseTabLabel, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) gobject.signal_new("reply", TweetBox, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) gobject.signal_new("retweet", TweetBox, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) my_twitter = MyTwitter() gtk.main()