#!/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.tweet_panes = [] self.tweets = [] self.list = None 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 # Add the Home tab to the notebook self.tweet_panes.append(TweetPane('Home', self)) self.tweet_notebook.append_page(self.tweet_panes[0]) self.tweet_notebook.set_tab_label_text(self.tweet_panes[0], '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): for pane in self.tweet_panes: 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) else: statuses = self.api.GetListStatuses(list_name, per_page=self.num_entries) pane.update_window(statuses) 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 set_tweets_read(self): print 'debug: set_tweets_read()' self.last_tweet_read = self.latest_tweet def set_tweets_read_callback(self, event): self.set_tweets_read() def on_view_selected(self, event, name): print 'debug: on_view_selected(): ' + 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.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) 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 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: 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 if num_new_tweets > 0: pass # fixme: change the label here, or maybe in mytwitter? def get_list_name(self): return self.list_name ### 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("#ffffff")) else: self.text_eb.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ddffdd")) 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 print 'debug: on_url_clicked()' subprocess.Popen('/usr/bin/firefox ' + self.text.get_current_uri(), shell=False).pid # end class TweetBox # main # Create custom events for TweetBox 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()