Commits

Josh VanderLinden committed 67c1c99

Finally updated the README. Added a domain cache for the short URL resolving business. Linked the core app with the GUI. Added commands: version, quit, friends, flush, prune, and interval. Tried to fix problems with connectivity a bit--may have made them worse :( Fixed the remind function to not send a Twitter update. Changed the authentication to use OAuth instead of relying on a local store of the whole world's username/password combinations. Fixed several config persistence issues. Bumped the version. Made the tray icon a bit bigger. Added a timeout feature, because some things take longer than I would like.

Comments (0)

Files changed (12)

-=================
-TWITTER IM (Twim)
-=================
+=======
+Twibber
+=======
 
 .. author: Josh VanderLinden <codekoala@gmail.com>
 .. Date: 28 May 2009
 
-Thanks for using Twim! The program is just an effort to make it easier (for me)
-to see when I have updates on Twitter.  It can log into one or more Twitter
+Thanks for using Twibber! The program is just an effort to make it easier (for 
+me) to see when I have updates on Twitter.  It can log into one or more Twitter
 accounts, periodically check for updates, and send those updates to your
 Jabber-compliant instant messenger program.
 
 You can also post updates to your Twitter from your instant messenger when
-sending messages to the Jabber user that you create.  Twim should be able to
+sending messages to the Jabber user that you create.  Twibber should be able to
 handle updates that are longer than 140 characters by splitting them up into
 separate updates.
 
 links to various items of interest, such as the user who posted the update,
 links to users with @replies, and links to hashtags.
 
+Twibber is capable of displaying the domain name of the final destination of
+all of those tinyurl.com, bit.ly, is.gd, etc sites.  This option can be 
+disabled if you so desire.  Along these same lines, Twibber will automatically
+replace long URLs in your Tweets with shorter URLs in order to save space.
+
 This is not meant to be a full-featured Twitter client.
 
 CONFIGURATION
 =============
 
-The first thing you need to do is download the latest copy of Twim from the
+The first thing you need to do is download the latest copy of Twibber from the
 following site:
 
     http://bitbucket.org/codekoala/twitter-im
     http://register.jabber.org/
 
 Be sure to add this special user to the buddy list of the Jabber user which
-will receive the Twitter updates from Twim.  Failure to do so will probably 
+will receive the Twitter updates from Twibber.  Failure to do so will probably 
 make you think the program is broken because you probably won't be receiving 
 updates the way you should.
 
-Next comes the configuration file.  For the time being, all configuration must
-take place manually--there is no GUI yet.  Make sure you run the program at
-least one time in order to make the configuration process go smoothly.  The
-first run will automatically create a configuration file for you with some
-decent defaults settings.
+Next comes the configuration file.  The configuration file should be located in
+your "home" directory, which differs depending on what platform you're using.
 
 On Windows XP, this file should be located at:
 
-    C:\Documents and Settings\[your username]\.twitterimrc
+    C:\Documents and Settings\[your username]\.twibberrc
 
 On Linux and OSX, it should be at:
 
-    ~/.twitterimrc
+    ~/.twibberrc
 
-Open this file in your favorite text editor and change the following settings
-to suit your needs:
-
-    username[0] = [your Twitter ID]
-    password[0] = [your Twitter password]
-    login_as_user = [special-user]@jabber.org
-    login_as_pass = [special-user's password]
-    send_to_user = [Jabber ID to send Twitter updated to]
+Open this file in your favorite text editor and change the settings to suit 
+your needs.  I don't feel like writing up how to define your `twitter_users`
+setting right now (it's 3AM), so you'll just have to hope the GUI works for you
 
 DO NOT MAKE `login_as_user` AND `send_to_user` THE SAME THING--YOU WILL SPAM
 THE WHOLE WORLD WITH JUNK THAT HAS ALREADY BEEN POSTED!!!!!  Also, if you set
 the `update_interval` setting to less than 35, you will probably run into your
 Twitter API hit limit and have blackouts every hour or so.  The more you post
-using Twim, the fewer API hits you have.  Regular users are allowed 100 hits
+using Twibber, the fewer API hits you have.  Regular users are allowed 100 hits
 an hour, which equates to one hit every ~35 seconds.
 
-If you're interested in checking multiple Twitter accounts at the same time,
-just add two lines for each account:
-
-    username[1] = [your next Twitter ID]
-    password[1] = [your next Twitter password]
-
-Make sure that you change the "1" in the square brackets to match the number
-of Twitter accounts that you have.
-
 USAGE
 =====
 
-Right now, using Twim is quite simple and basic.  The most interesting thing
-that you can probably do with it is post Twitter updates as a different user
-if you have multiple Twitter accounts setup.  To do so, simply send a message
-to your special Jabber ID that resembles the following:
+Keeping this readme file up-to-date with all of the latest and greatest goodies
+that I put into Twibber is not only very time-consuming, it's also very 
+redundant.  Once you get Twibber running, simply issue commands to it from your
+instant messenger program.
 
-    ./as otheruser I'm posting as a different user!
+All commands must begin with "./"--without this they will not be interpretted 
+as commands and will be sent to Twitter as a status update.  Yeah.
 
-Assuming that "otheruser" is in your configuration file and you are allowed
-to post, such a message will cause Twim to post an update to Twitter as
-"otheruser" with the text, "I'm posting as a different user!"
+For a comprehensive list of commands, just type "./help" and a list will appear.
+For further information about a particular command, just type it after the help
+command.  For example if I wanted to learn more about the "limits" command, I 
+would do:
+
+    ./help limits
+
+You may also abbreviate your commands.  Since "help" is currently the only 
+command that begins with "h", you can type "./h" and Twibber will know what to
+do.  If you try to do "./r", Twibber will complain because there are several
+commands which begin with "r" (retweet, reply, rt, remind, etc).  These same
+abbreviations may be used in conjunction with the "help" command:
+
+    ./h rt
+
+Such a command will display the information for the "retweet" command.
 import unicodedata
 import urllib
 import urllib2
+import utils
 import xmpp
 from datetime import datetime, timedelta
 from data import cache, config
     _commands = {}
     _history = []
     _queue = []
+    _gui = None
+    _domains = {}
 
     def __init__(self, with_gui=True):
         log.info('Starting app on %s' % sys.platform)
         else:
             self.BeginBeingUseful()
 
-    def BeginBeingUseful(self):
+    def BeginBeingUseful(self, gui=None):
         """
         Things to do after initial startup
         """
         log.info('Performing post-init startup')
-        self.InitCommands()
-        self.Connect()
+        self._gui = gui
+        self.OnConfigChanged()
 
         # create some threads so we don't have an unresponsive program
         log.info('starting threads')
         self.check_timer.start()
         self.post_timer.start()
 
+    def OnConfigChanged(self):
+        """
+        Handles the reconfiguration of Twibber
+        """
+        self.InitCommands()
+        self.Connect(force=True)
+
     def InitCommands(self):
         """
         Initializes the commands that are available to the user
             'message': self.OnDirectMessageCommand,
             'dm': self.OnDirectMessageCommand,
             'limits': self.OnRateLimitsCommand,
-            'quit': self.OnQuitCommand
+            'version': self.OnVersionCommand,
+            'quit': self.OnQuitCommand,
+            'friends': self.OnFriendsCommand,
+            'flush': self.OnFlushCommand,
+            'prune': self.OnPruneCommand,
+            'interval': self.OnIntervalCommand
         }
 
         # enable the "as" command if we have multiple users setup
         if len(config.users) > 1:
             self._commands['as'] = self.OnAsCommand
 
-    def Connect(self):
+    def Connect(self, force=False):
         """
         Attempts to establish a connection with Jabber
         """
 
         try:
+            if force:
+                self._client = None
+
             # handle messages sent by the user
             self.client.RegisterHandler('message', self.OnMessage)
+            self.client.RegisterHandler('presence', self.OnMessage)
 
             # tell people we're online
             self.client.sendInitPresence()
-            self.presence = xmpp.Presence(priority=5, show='chat')
+            self.presence = xmpp.Presence(show='chat',
+                                          status='Doing thy bidding, master.')
             self.client.send(self.presence)
         except Exception, ex:
             log.error(ex)
         return self._client
     client = property(GetClient)
 
-    def OnExit(self, evt=None):
+    def OnExit(self, evt=None, from_gui=False):
         """
         Shut everything down
         """
         config.persist()
 
+        if self._gui and not from_gui:
+            self._gui.OnExit(from_core=True)
+
         log.info('Terminating the application')
         self.checker.set()
         self.poster.set()
             cmd = self.FindCommand(cmd)
             if cmd in self._commands.keys():
                 func = self._commands[cmd]
-                self.TellUser('%s usage:\n%s' % (cmd, func.__doc__))
+                self.TellUser('%s Help:\n%s' % (cmd, func.__doc__))
             else:
                 self.TellUser('Unknown command "%s"' % cmd)
         else:
                 now = datetime.now()
 
                 if 'delta' in args.keys():
-                    unit = args['unit']
+                    unit = {'minute': 'minutes',
+                            'second': 'seconds',
+                            'hour': 'hours',
+                            'day': 'days'}.get(args['unit'], args['unit'])
                     value = int(args['delta'])
-                    delta = timedelta(**{unit: value})
+                    delta = timedelta(**{str(unit): value})
                     schedule_at = now + delta
                 else:
                     for key, val in args.items():
             tweet = cache.by_id(tid)
             if tweet:
                 text = tweet.text
-                
+
                 # make it possible to change the retweet
                 if len(comments) >= len(tweet.text):
                     text = comments
 
     def OnReplyCommand(self, user, body, remaining):
         """
-        The reply command allows you to reply to a particular tweet.  For 
-        example, if you wanted to reply to something that @ev said about Maui, 
+        The reply command allows you to reply to a particular tweet.  For
+        example, if you wanted to reply to something that @ev said about Maui,
         you would do this:
 
         ev: [ 0 ] In Maui. Wowee.
     def OnFavoriteCommand(self, user, body, remaining):
         """
         The favorite command allows you to mark a particular tweet as one of
-        your favorite.  For example, if you wanted to mark something that @ev 
+        your favorite.  For example, if you wanted to mark something that @ev
         said about Maui, you would do:
 
         ev: [ 0 ] In Maui. Wowee.
     def OnUnfavoriteCommand(self, user, body, remaining):
         """
         The unfavorite command allows you to remove a particular tweet as one
-        of your favorite.  For example, if you wanted to unfavorite something 
+        of your favorite.  For example, if you wanted to unfavorite something
         that @ev said about Maui, you would do:
 
         ev: [ 0 ] In Maui. Wowee.
 
     def OnQuitCommand(self, user, body, remaining):
         """
-        The quit command will remotely shut down Twibber.
+        The quit command will remotely shut down Twibber.  This should only
+        really be used when running with no GUI.
         """
+        self.TellUser('Bye!')
         self.OnExit()
 
+    def OnVersionCommand(self, user, body, remaining):
+        """
+        The version command will tell you what version of Twibber you're using
+        """
+        from twibber import APP_TITLE, __version__
+        self.TellUser("You're using %s v%s!" % (APP_TITLE, __version__))
+
+    def OnFlushCommand(self, user, body, remaining):
+        """
+        The flush command will clear our Twibber's cache.  Good way to free up
+        some memory if it becomes a problem.
+        """
+        cache.flush()
+        self.TellUser('The cache has been flushed.')
+
+    def OnPruneCommand(self, user, body, remaining):
+        """
+        The prune command will clear out some of your cache, but not all of it
+        (unless you don't have much in your cache).  You may optionally tell
+        Twibber how much of your cache you would like pruned (defaults to 100
+        entries).
+        """
+        args = re.search('^(\d+)', remaining)
+        if args:
+            to_prune = args.group(1)
+            results = cache.prune(int(to_prune))
+        results = cache.prune()
+
+        self.TellUser('Pruned %i entries from cache.' % results)
+
+    def OnIntervalCommand(self, user, body, remaining):
+        """
+        The interval command allows you to change the interval at which Twibber
+        checks for your Twitter updates.  The interval is in seconds (not minutes
+        or milliseconds).  Be very careful with this, as setting your update
+        interval to 40 seconds or less will likely exhaust your Twitter API
+        limitations very quickly.  Use the interval command in conjunction with
+        the limits command.
+        """
+        args = re.search('^(\d+)', remaining)
+        if args:
+            interval = int(args.group(1))
+            if interval <= 35:
+                self.TellUser(
+                    "Ehh, not today soldier!  You don't really want your "
+                    "interval to be that small.  Trust me.")
+            else:
+                self.TellUser('Update interval changing from %i to %i.' % (
+                                config.update_interval, interval))
+                config.update_interval = interval
+                self.interval = timedelta(seconds=interval)
+        else:
+            self.TellUser('Current Interval: %i' % config.update_interval)
+
+    def OnFriendsCommand(self, user, body, remaining):
+        """
+        The friends command will tell you the screen name for any of your
+        friends based on the prefix you provide.  This will help you when you
+        forget who is who.
+        """
+
+        # get all users for this user... this isn't rate-limited, so it doesn't
+        # hurt to not cache it
+        all = []
+        loop = True
+        page = 1
+        while loop:
+            friends = user.api.GetFriends(page=page)
+            all.extend(friends)
+            page += 1
+            if len(friends) <= 0:
+                loop = False
+
+        friends = [f.screen_name for f in all if f.screen_name.startswith(remaining)]
+        self.TellUser("Friends whose screen name begins with: %s\n%s" % (
+                            remaining, ', '.join(friends)))
+
     def FindCommand(self, text):
         """
         Attempts to find a Twibber command based on the specified text.  The
 
         return command
 
+    def OnPresence(self, client, evt):
+        """
+        Handles requests for a user to add the courier to their roster
+        """
+        sender = evt.getFrom().getStripped()
+
+        # don't accept presence events from anyone but the user we sent tweets to
+        if sender != config.send_to_user: return
+
+        sub_types = ('subscribe', 'subscribed')
+
+        # this should accept a buddy request from the user to whom we send
+        # tweets
+        if evt.getType() in sub_types:
+            for typ in sub_types:
+                self.client.send(xmpp.Presence(to=config.send_to_user, typ=typ))
+            return
+
     def OnMessage(self, client, evt):
         """
         Handles messages sent by the user.  This will post updates to Twitter
         if sender != config.send_to_user: return
 
         # make sure this is a valid message
-        sub_types = ('subscribe', 'subscribed')
-        valid_types = sub_types + ('message', 'chat', None)
+        valid_types = ('message', 'chat', 'normal', None)
         if evt.getType() not in valid_types: return
 
-        # this should accept a buddy request from the user to whom we send
-        # tweets
-        if evt.getType() in sub_types:
-            for typ in sub_types:
-                self.client.send(xmpp.Presence(to=config.send_to_user, typ=typ))
-            return
-
         as_user = config.default_user
         body = body.strip()
         if body.startswith('./'):
                 action()
             except Exception, ex:
                 log.error(ex)
-                self.TellUser('We are experiencing problems... please be patient.')
+                #self.TellUser('We are experiencing problems... please be patient.')
 
     def CheckForUpdates(self):
         """
                     self.GetUpdatesFor(user)
         except urllib2.HTTPError, ex:
             log.error(ex)
-            self._client = None
-            self.GetClient()
             time.sleep(5)
 
     def CheckForPosts(self):
         # run through any scheduled tweets to see if it's time to post
         for tweet in config.due_tweets:
             log.info('Time to tweet "%s"' % tweet.text)
-            self.PostUpdate(config.users[tweet.as_user], tweet.text)
+            if tweet.is_reminder:
+                self.TellUser(tweet.text)
+            else:
+                self.PostUpdate(config.users[tweet.as_user], tweet.text)
             config._scheduled.remove(tweet)
 
         self.client.Process(1)
         inserted into the text if it differs from the domain in the original URL
         """
 
-        # TODO make this optional
+        if not config.display_destination:
+            return text
+
         links = HREF_RE.findall(text)
         for link in links:
             try:
-                log.info('Trying to resolve domain name for %s' % link[0])
-                con = urllib2.urlopen(link[0])
-                dest = con.geturl()
-                match = HREF_RE.search(dest)
-                if match:
-                    url, domain, path = match.groups()
-                    if domain != link[1]:
-                        rep = '%s [%s]' % (link[0], domain)
-                        text = text.replace(link[0], rep)
+                log.info('Getting destination domain name for %s' % link[0])
+
+                # check our cache first
+                domain = self._domains.get(links[0], None)
+                if not domain:
+                    # if it's not in the cache try to get it
+                    # TODO add a timeout: http://2ze.us/jp
+                    log.info('%s is not in cache; fetching it...' % link[0])
+
+                    def _get_domain(url):
+                        con = urllib2.urlopen(url)
+                        dest = con.geturl()
+                        match = HREF_RE.search(dest)
+                        domain = None
+                        if match:
+                            url, domain, path = match.groups()
+                        return domain
+
+                    domain = utils.timeout(_get_domain,
+                                           kwargs={'url': link[0]},
+                                           timeout_duration=7)
+                    if domain:
+                        # put it in the cache
+                        self._domains[link[0]] = domain
+
+                # if the destination domain differs, make it obvious
+                if domain and domain != link[1]:
+                    rep = '%s [%s]' % (link[0], domain)
+                    text = text.replace(link[0], rep)
             except Exception, ex:
                 # it's not that important...
                 log.error(ex)
 import logging
 import os
 import pickle
+import time
 import twitter
+import urllib2
+import webbrowser
 from ConfigParser import SafeConfigParser
 from datetime import datetime
+from oauthtwitter import OAuthApi, oauth
 
 log = logging.getLogger('twibber.db')
 
 HOME_DIR = os.path.expanduser('~')
 CONFIG = os.path.join(HOME_DIR, '.twibberrc')
 
+CONSUMER_KEY = 'ONaGpSBGFNq2ouVtxGyw'
+CONSUMER_SECRET = 'Z6UMuHhDCQ0WLYdcZelA4R3K8OXxsAE0bduYWt6U'
+
 class User(object):
     """
     Represents a Twitter user in Twibber.
     """
-    __slots__ = ('username', '_password', 'last_tweet_id', 'last_update', '_api')
+    __slots__ = ('username', '_password', 'last_tweet_id', 'last_update',
+                 '_api', '_request_token', '_auth_url', '_access_token')
 
-    def __init__(self, username, password='', last_tweet_id=0,
+    def __init__(self, username=None, password='', last_tweet_id=0,
                  last_update=None):
         self.username = username
         self._set_password(password)
         self.last_tweet_id = last_tweet_id
         self.last_update = last_update
         self._api = None
+        self._request_token = None
+        self._auth_url = None
+        self._access_token = None
 
     def __unicode__(self):
         return self.username
         if not self._api:
             import twibber
             # connect to Twitter for this user
-            self._api = twitter.Api(username=self.username,
-                                    password=self.password)
+            if self.username and self.password:
+                self._api = twitter.Api(username=self.username,
+                                        password=self.password)
+            else:
+                self.BeginOAuth()
+                self.CompleteOAuth()
+
             self._api.SetSource(twibber.APP_TITLE)
             self._api.SetXTwitterHeaders(twibber.APP_TITLE,
                                          twibber.__url__,
         self._password = base64.b64encode(value)
     password = property(_get_password, _set_password)
 
+    def BeginOAuth(self, manual=True):
+        """
+        Authorizes the user using OAuth
+        """
+        if not self._auth_url:
+            api = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET)
+            self._request_token = api.getRequestToken()
+            self._auth_url = api.getAuthorizationURL(self._request_token)
+
+        log.debug('Heading to ' + self._auth_url)
+        if manual:
+            webbrowser.open(self._auth_url)
+
+    def CompleteOAuth(self):
+        """
+        This must be called after we have the access key
+        """
+        if not self._access_token:
+            api = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET, self._request_token)
+            self._access_token = api.getAccessToken()
+
+        if self._access_token:
+            api = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET, self._access_token)
+            self.username = api.GetUserInfo().screen_name
+            self._api = api
+            return api
+
+    def serialize(self):
+        """
+        Serializes this user's information so it can hang out in the config
+        """
+        data = [
+            self.username or '',
+            self._password or '',
+            str(self.last_tweet_id) or '',
+            self._request_token.to_string() or '',
+            self._auth_url or '',
+            self._access_token.to_string() or ''
+        ]
+        return base64.b64encode('&&&'.join(data))
+
+    def deserialize(cls, value):
+        """
+        Deserializes this user's information
+        """
+        data = base64.b64decode(value)
+        user = User()
+        if data:
+            data = data.split('&&&')
+            log.debug(data)
+            user.username = data[0]
+            user._password = data[1]
+            user.last_tweet_id = int(data[2])
+
+            if len(data[3]):
+                user._request_token = oauth.OAuthToken.from_string(data[3])
+            user._auth_url = data[4]
+
+            if len(data[5]):
+                user._access_token = oauth.OAuthToken.from_string(data[5])
+            log.debug(data)
+            
+            user.BeginOAuth(manual=False)
+            user.CompleteOAuth()
+        return user
+    deserialize = classmethod(deserialize)
+
 class ScheduledTweet(object):
     """
     Represents a tweet that is scheduled to be sent at some point in the future
     """
-    __slots__ = ('as_user', 'text', 'send_at', 'is_reminder')
-
     def __init__(self, user, text, send_at, is_reminder=False):
         self.as_user = user
         self.text = text
     Defaults = {
         'update_interval': (90, int),
         'last_tweet_id': (0, int),
+        'display_destination': (True, bool),
         'login_as_user': '',
         'login_as_pass': '',
         'send_to_user': '',
 
         # grab these values from the configuration
         self.to_grab = ('update_interval', 'last_tweet_id', 'login_as_user',
-                        'login_as_pass', 'send_to_user', 'default_user')
+                        'login_as_pass', 'send_to_user', 'display_destination')
 
         log.debug('Reading configuration from: %s' % self.configfile)
         self.parser = SafeConfigParser()
         """
         if not self._users:
             self._users = {}
-            for info in self.get('twitter_users').split('|'):
-                if len(info) > 3:
-                    # need at least three characters
-                    username, passwd = info.split(':')
-                    u = User(username)
-                    u._password = passwd
-                    self._users[username] = u
+            for user_s in self.get('twitter_users').split('|'):
+                user = User.deserialize(user_s)
+                if user:
+                    self._users[user.username] = user
         return self._users
 
     def _set_users(self, users):
         """
         assert isinstance(users, dict)
 
-        kvp = ['%s:%s' % (u.username, u._password) for u in users.values()]
-        self.set('twitter_users', '|'.join(kvp))
+        self._users = users
+        serial = [u.serialize() for u in users.values()]
+        self.set('twitter_users', '|'.join(serial))
     users = property(_get_users, _set_users)
 
     def _get_filtered_tags(self):
             if '' in self._filtered:
                 self._filtered.remove('')
         return self._filtered
-    
+
     def _set_filtered_tags(self, tags):
         """
         Sets the value for the filtered tags
         Returns the default Twitter user
         """
         if not self._default_user:
-            user = self.users.values()[0]
+            count = len(self.users)
+            user = None
+            if count != 0:
+                user = self.users.values()[0]
+
             if len(self.users) > 1:
                 user = self.users.get(self.get('default_user'), user)
+
             self._default_user = user
         return self._default_user
 
         """
         Sets the default user's username in the configuration file
         """
-        user = self.default_user
+        if isinstance(user, str):
+            user = self.users.get(user, None)
+
+        self._default_user = user
         if user:
             self.set('default_user', user.username)
     default_user = property(_get_default_user, _set_default_user)
         Writes changes to the config to disk.
         """
         log.info('Saving configuration')
-        self.users = self.users
-        self.scheduled_tweets = self.scheduled_tweets
-        self.filtered_tags = self.filtered_tags
-
-        for key in self.to_grab:
-            self.set(key, getattr(self, key, self.get_default(key)))
-
-        out = open(self.configfile, 'wb')
-        self.parser.write(out)
-        out.close()
+        try:
+            self._set_users(self.users)
+            self.scheduled_tweets = self.scheduled_tweets
+            self.filtered_tags = self.filtered_tags
+            self.default_user = self.default_user
+    
+            for key in self.to_grab:
+                self.set(key, getattr(self, key, self.get_default(key)))
+    
+            out = open(self.configfile, 'wb')
+            self.parser.write(out)
+            out.close()
+        except Exception, ex:
+            log.error('Persist Error: %s' % ex)
 
 class Cache(object):
-    _tweets = {}
-    _counter_map = {}
-    _tweet_map = {}
-    _counter = 1
+    def __init__(self):
+        self.flush()
 
     def set(self, tweet):
         """
         """
         return self._tweet_map.get(int(id), None)
 
+    def prune(self, count=100):
+        """
+        Prune out old information to free memory
+        """
+
+        # find the earliest counter ID
+        ids = self._counter_map.keys()
+        ids.sort()
+        earliest = ids[0]
+        pruned = 0
+
+        for counter in xrange(earliest, earliest + count + 1):
+            tweet_id = self._counter_map.get(counter, 0)
+            try:
+                del self._tweets[tweet_id]
+                del self._counter_map[counter]
+                del self._tweet_map[tweet_id]
+                pruned += 1
+            except:
+                pass
+
+        return pruned
+
+    def flush(self):
+        """
+        Flushes the cache and starts over.  Useful for freeing up memory.
+        """
+        self._tweets = {}
+        self._counter_map = {}
+        self._tweet_map = {}
+        self._counter = 1
+
 config = Config(CONFIG)
-cache = Cache()
+cache = Cache()
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import cgi
+import urllib
+import time
+import random
+import urlparse
+import hmac
+import binascii
+
+
+VERSION = '1.0' # Hi Blaine!
+HTTP_METHOD = 'GET'
+SIGNATURE_METHOD = 'PLAINTEXT'
+
+
+class OAuthError(RuntimeError):
+    """Generic exception class."""
+    def __init__(self, message='OAuth error occured.'):
+        self.message = message
+
+def build_authenticate_header(realm=''):
+    """Optional WWW-Authenticate header (401 error)"""
+    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+def escape(s):
+    """Escape a URL including any /."""
+    return urllib.quote(s, safe='~')
+
+def _utf8_str(s):
+    """Convert unicode to utf-8."""
+    if isinstance(s, unicode):
+        return s.encode("utf-8")
+    else:
+        return str(s)
+
+def generate_timestamp():
+    """Get seconds since epoch (UTC)."""
+    return int(time.time())
+
+def generate_nonce(length=8):
+    """Generate pseudorandom number."""
+    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+class OAuthConsumer(object):
+    """Consumer of OAuth authentication.
+
+    OAuthConsumer is a data type that represents the identity of the Consumer
+    via its shared secret with the Service Provider.
+
+    """
+    key = None
+    secret = None
+
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+   
+class OAuthToken(object):
+    """OAuthToken is a data type that represents an End User via either an access
+    or request token.
+    
+    key -- the token
+    secret -- the token secret
+
+    """
+    key = None
+    secret = None
+
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+    def to_string(self):
+        return urllib.urlencode({'oauth_token': self.key,
+            'oauth_token_secret': self.secret})
+ 
+    def from_string(s):
+        """ Returns a token from something like:
+        oauth_token_secret=xxx&oauth_token=xxx
+        """
+        params = cgi.parse_qs(s, keep_blank_values=False)
+        key = params['oauth_token'][0]
+        secret = params['oauth_token_secret'][0]
+        return OAuthToken(key, secret)
+    from_string = staticmethod(from_string)
+
+    def __str__(self):
+        return self.to_string()
+
+
+class OAuthRequest(object):
+    """OAuthRequest represents the request and can be serialized.
+
+    OAuth parameters:
+        - oauth_consumer_key 
+        - oauth_token
+        - oauth_signature_method
+        - oauth_signature 
+        - oauth_timestamp 
+        - oauth_nonce
+        - oauth_version
+        ... any additional parameters, as defined by the Service Provider.
+    """
+    parameters = None # OAuth parameters.
+    http_method = HTTP_METHOD
+    http_url = None
+    version = VERSION
+
+    def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
+        self.http_method = http_method
+        self.http_url = http_url
+        self.parameters = parameters or {}
+
+    def set_parameter(self, parameter, value):
+        self.parameters[parameter] = value
+
+    def get_parameter(self, parameter):
+        try:
+            return self.parameters[parameter]
+        except:
+            raise OAuthError('Parameter not found: %s' % parameter)
+
+    def _get_timestamp_nonce(self):
+        return self.get_parameter('oauth_timestamp'), self.get_parameter(
+            'oauth_nonce')
+
+    def get_nonoauth_parameters(self):
+        """Get any non-OAuth parameters."""
+        parameters = {}
+        for k, v in self.parameters.iteritems():
+            # Ignore oauth parameters.
+            if k.find('oauth_') < 0:
+                parameters[k] = v
+        return parameters
+
+    def to_header(self, realm=''):
+        """Serialize as a header for an HTTPAuth request."""
+        auth_header = 'OAuth realm="%s"' % realm
+        # Add the oauth parameters.
+        if self.parameters:
+            for k, v in self.parameters.iteritems():
+                if k[:6] == 'oauth_':
+                    auth_header += ', %s="%s"' % (k, escape(str(v)))
+        return {'Authorization': auth_header}
+
+    def to_postdata(self):
+        """Serialize as post data for a POST request."""
+        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
+            for k, v in self.parameters.iteritems()])
+
+    def to_url(self):
+        """Serialize as a URL for a GET request."""
+        return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
+
+    def get_normalized_parameters(self):
+        """Return a string that contains the parameters that must be signed."""
+        params = self.parameters
+        try:
+            # Exclude the signature if it exists.
+            del params['oauth_signature']
+        except:
+            pass
+        # Escape key values before sorting.
+        key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
+            for k,v in params.items()]
+        # Sort lexicographically, first after key, then after value.
+        key_values.sort()
+        # Combine key value pairs into a string.
+        return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
+
+    def get_normalized_http_method(self):
+        """Uppercases the http method."""
+        return self.http_method.upper()
+
+    def get_normalized_http_url(self):
+        """Parses the URL and rebuilds it to be scheme://host/path."""
+        parts = urlparse.urlparse(self.http_url)
+        scheme, netloc, path = parts[:3]
+        # Exclude default port numbers.
+        if scheme == 'http':
+            netloc = netloc.rstrip(':80')
+        elif scheme == 'https':
+            netloc = netloc.rstrip(':443')
+        return '%s://%s%s' % (scheme, netloc, path)
+
+    def sign_request(self, signature_method, consumer, token):
+        """Set the signature parameter to the result of build_signature."""
+        # Set the signature method.
+        self.set_parameter('oauth_signature_method',
+            signature_method.get_name())
+        # Set the signature.
+        self.set_parameter('oauth_signature',
+            self.build_signature(signature_method, consumer, token))
+
+    def build_signature(self, signature_method, consumer, token):
+        """Calls the build signature method within the signature method."""
+        return signature_method.build_signature(self, consumer, token)
+
+    def from_request(http_method, http_url, headers=None, parameters=None,
+            query_string=None):
+        """Combines multiple parameter sources."""
+        if parameters is None:
+            parameters = {}
+
+        # Headers
+        if headers and 'Authorization' in headers:
+            auth_header = headers['Authorization']
+            # Check that the authorization header is OAuth.
+            if auth_header.index('OAuth') > -1:
+                auth_header = auth_header.lstrip('OAuth ')
+                try:
+                    # Get the parameters from the header.
+                    header_params = OAuthRequest._split_header(auth_header)
+                    parameters.update(header_params)
+                except:
+                    raise OAuthError('Unable to parse OAuth parameters from '
+                        'Authorization header.')
+
+        # GET or POST query string.
+        if query_string:
+            query_params = OAuthRequest._split_url_string(query_string)
+            parameters.update(query_params)
+
+        # URL parameters.
+        param_str = urlparse.urlparse(http_url)[4] # query
+        url_params = OAuthRequest._split_url_string(param_str)
+        parameters.update(url_params)
+
+        if parameters:
+            return OAuthRequest(http_method, http_url, parameters)
+
+        return None
+    from_request = staticmethod(from_request)
+
+    def from_consumer_and_token(oauth_consumer, token=None,
+            http_method=HTTP_METHOD, http_url=None, parameters=None):
+        if not parameters:
+            parameters = {}
+
+        defaults = {
+            'oauth_consumer_key': oauth_consumer.key,
+            'oauth_timestamp': generate_timestamp(),
+            'oauth_nonce': generate_nonce(),
+            'oauth_version': OAuthRequest.version,
+        }
+
+        defaults.update(parameters)
+        parameters = defaults
+
+        if token:
+            parameters['oauth_token'] = token.key
+
+        return OAuthRequest(http_method, http_url, parameters)
+    from_consumer_and_token = staticmethod(from_consumer_and_token)
+
+    def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
+            http_url=None, parameters=None):
+        if not parameters:
+            parameters = {}
+
+        parameters['oauth_token'] = token.key
+
+        if callback:
+            parameters['oauth_callback'] = callback
+
+        return OAuthRequest(http_method, http_url, parameters)
+    from_token_and_callback = staticmethod(from_token_and_callback)
+
+    def _split_header(header):
+        """Turn Authorization: header into parameters."""
+        params = {}
+        parts = header.split(',')
+        for param in parts:
+            # Ignore realm parameter.
+            if param.find('realm') > -1:
+                continue
+            # Remove whitespace.
+            param = param.strip()
+            # Split key-value.
+            param_parts = param.split('=', 1)
+            # Remove quotes and unescape the value.
+            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+        return params
+    _split_header = staticmethod(_split_header)
+
+    def _split_url_string(param_str):
+        """Turn URL string into parameters."""
+        parameters = cgi.parse_qs(param_str, keep_blank_values=False)
+        for k, v in parameters.iteritems():
+            parameters[k] = urllib.unquote(v[0])
+        return parameters
+    _split_url_string = staticmethod(_split_url_string)
+
+class OAuthServer(object):
+    """A worker to check the validity of a request against a data store."""
+    timestamp_threshold = 300 # In seconds, five minutes.
+    version = VERSION
+    signature_methods = None
+    data_store = None
+
+    def __init__(self, data_store=None, signature_methods=None):
+        self.data_store = data_store
+        self.signature_methods = signature_methods or {}
+
+    def set_data_store(self, data_store):
+        self.data_store = data_store
+
+    def get_data_store(self):
+        return self.data_store
+
+    def add_signature_method(self, signature_method):
+        self.signature_methods[signature_method.get_name()] = signature_method
+        return self.signature_methods
+
+    def fetch_request_token(self, oauth_request):
+        """Processes a request_token request and returns the
+        request token on success.
+        """
+        try:
+            # Get the request token for authorization.
+            token = self._get_token(oauth_request, 'request')
+        except OAuthError:
+            # No token required for the initial token request.
+            version = self._get_version(oauth_request)
+            consumer = self._get_consumer(oauth_request)
+            self._check_signature(oauth_request, consumer, None)
+            # Fetch a new token.
+            token = self.data_store.fetch_request_token(consumer)
+        return token
+
+    def fetch_access_token(self, oauth_request):
+        """Processes an access_token request and returns the
+        access token on success.
+        """
+        version = self._get_version(oauth_request)
+        consumer = self._get_consumer(oauth_request)
+        # Get the request token.
+        token = self._get_token(oauth_request, 'request')
+        self._check_signature(oauth_request, consumer, token)
+        new_token = self.data_store.fetch_access_token(consumer, token)
+        return new_token
+
+    def verify_request(self, oauth_request):
+        """Verifies an api call and checks all the parameters."""
+        # -> consumer and token
+        version = self._get_version(oauth_request)
+        consumer = self._get_consumer(oauth_request)
+        # Get the access token.
+        token = self._get_token(oauth_request, 'access')
+        self._check_signature(oauth_request, consumer, token)
+        parameters = oauth_request.get_nonoauth_parameters()
+        return consumer, token, parameters
+
+    def authorize_token(self, token, user):
+        """Authorize a request token."""
+        return self.data_store.authorize_request_token(token, user)
+
+    def get_callback(self, oauth_request):
+        """Get the callback URL."""
+        return oauth_request.get_parameter('oauth_callback')
+ 
+    def build_authenticate_header(self, realm=''):
+        """Optional support for the authenticate header."""
+        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+    def _get_version(self, oauth_request):
+        """Verify the correct version request for this server."""
+        try:
+            version = oauth_request.get_parameter('oauth_version')
+        except:
+            version = VERSION
+        if version and version != self.version:
+            raise OAuthError('OAuth version %s not supported.' % str(version))
+        return version
+
+    def _get_signature_method(self, oauth_request):
+        """Figure out the signature with some defaults."""
+        try:
+            signature_method = oauth_request.get_parameter(
+                'oauth_signature_method')
+        except:
+            signature_method = SIGNATURE_METHOD
+        try:
+            # Get the signature method object.
+            signature_method = self.signature_methods[signature_method]
+        except:
+            signature_method_names = ', '.join(self.signature_methods.keys())
+            raise OAuthError('Signature method %s not supported try one of the '
+                'following: %s' % (signature_method, signature_method_names))
+
+        return signature_method
+
+    def _get_consumer(self, oauth_request):
+        consumer_key = oauth_request.get_parameter('oauth_consumer_key')
+        consumer = self.data_store.lookup_consumer(consumer_key)
+        if not consumer:
+            raise OAuthError('Invalid consumer.')
+        return consumer
+
+    def _get_token(self, oauth_request, token_type='access'):
+        """Try to find the token for the provided request token key."""
+        token_field = oauth_request.get_parameter('oauth_token')
+        token = self.data_store.lookup_token(token_type, token_field)
+        if not token:
+            raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
+        return token
+
+    def _check_signature(self, oauth_request, consumer, token):
+        timestamp, nonce = oauth_request._get_timestamp_nonce()
+        self._check_timestamp(timestamp)
+        self._check_nonce(consumer, token, nonce)
+        signature_method = self._get_signature_method(oauth_request)
+        try:
+            signature = oauth_request.get_parameter('oauth_signature')
+        except:
+            raise OAuthError('Missing signature.')
+        # Validate the signature.
+        valid_sig = signature_method.check_signature(oauth_request, consumer,
+            token, signature)
+        if not valid_sig:
+            key, base = signature_method.build_signature_base_string(
+                oauth_request, consumer, token)
+            raise OAuthError('Invalid signature. Expected signature base '
+                'string: %s' % base)
+        built = signature_method.build_signature(oauth_request, consumer, token)
+
+    def _check_timestamp(self, timestamp):
+        """Verify that timestamp is recentish."""
+        timestamp = int(timestamp)
+        now = int(time.time())
+        lapsed = now - timestamp
+        if lapsed > self.timestamp_threshold:
+            raise OAuthError('Expired timestamp: given %d and now %s has a '
+                'greater difference than threshold %d' %
+                (timestamp, now, self.timestamp_threshold))
+
+    def _check_nonce(self, consumer, token, nonce):
+        """Verify that the nonce is uniqueish."""
+        nonce = self.data_store.lookup_nonce(consumer, token, nonce)
+        if nonce:
+            raise OAuthError('Nonce already used: %s' % str(nonce))
+
+
+class OAuthClient(object):
+    """OAuthClient is a worker to attempt to execute a request."""
+    consumer = None
+    token = None
+
+    def __init__(self, oauth_consumer, oauth_token):
+        self.consumer = oauth_consumer
+        self.token = oauth_token
+
+    def get_consumer(self):
+        return self.consumer
+
+    def get_token(self):
+        return self.token
+
+    def fetch_request_token(self, oauth_request):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def fetch_access_token(self, oauth_request):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def access_resource(self, oauth_request):
+        """-> Some protected resource."""
+        raise NotImplementedError
+
+
+class OAuthDataStore(object):
+    """A database abstraction used to lookup consumers and tokens."""
+
+    def lookup_consumer(self, key):
+        """-> OAuthConsumer."""
+        raise NotImplementedError
+
+    def lookup_token(self, oauth_consumer, token_type, token_token):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def fetch_request_token(self, oauth_consumer):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def fetch_access_token(self, oauth_consumer, oauth_token):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def authorize_request_token(self, oauth_token, user):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+
+class OAuthSignatureMethod(object):
+    """A strategy class that implements a signature method."""
+    def get_name(self):
+        """-> str."""
+        raise NotImplementedError
+
+    def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
+        """-> str key, str raw."""
+        raise NotImplementedError
+
+    def build_signature(self, oauth_request, oauth_consumer, oauth_token):
+        """-> str."""
+        raise NotImplementedError
+
+    def check_signature(self, oauth_request, consumer, token, signature):
+        built = self.build_signature(oauth_request, consumer, token)
+        return built == signature
+
+
+class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
+
+    def get_name(self):
+        return 'HMAC-SHA1'
+        
+    def build_signature_base_string(self, oauth_request, consumer, token):
+        sig = (
+            escape(oauth_request.get_normalized_http_method()),
+            escape(oauth_request.get_normalized_http_url()),
+            escape(oauth_request.get_normalized_parameters()),
+        )
+
+        key = '%s&' % escape(consumer.secret)
+        if token:
+            key += escape(token.secret)
+        raw = '&'.join(sig)
+        return key, raw
+
+    def build_signature(self, oauth_request, consumer, token):
+        """Builds the base signature string."""
+        key, raw = self.build_signature_base_string(oauth_request, consumer,
+            token)
+
+        # HMAC object.
+        try:
+            import hashlib # 2.5
+            hashed = hmac.new(key, raw, hashlib.sha1)
+        except:
+            import sha # Deprecated
+            hashed = hmac.new(key, raw, sha)
+
+        # Calculate the digest base 64.
+        return binascii.b2a_base64(hashed.digest())[:-1]
+
+
+class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
+
+    def get_name(self):
+        return 'PLAINTEXT'
+
+    def build_signature_base_string(self, oauth_request, consumer, token):
+        """Concatenates the consumer key and secret."""
+        sig = '%s&' % escape(consumer.secret)
+        if token:
+            sig = sig + escape(token.secret)
+        return sig, sig
+
+    def build_signature(self, oauth_request, consumer, token):
+        key, raw = self.build_signature_base_string(oauth_request, consumer,
+            token)
+        return key
Binary file added.
+#!/usr/bin/env python
+# 
+# Copyright under GPLv3
+
+'''A class the inherits everything from python-twitter and allows oauth based access
+
+Requires:
+  python-twitter
+  simplejson
+  oauth
+'''
+
+__author__ = "Hameedullah Khan <hameed@hameedkhan.net>"
+__version__ = "0.1"
+
+
+from twitter import Api, User
+
+import simplejson, oauth
+
+
+
+# Taken from oauth implementation at: http://github.com/harperreed/twitteroauth-python/tree/master
+REQUEST_TOKEN_URL = 'https://twitter.com/oauth/request_token'
+ACCESS_TOKEN_URL = 'https://twitter.com/oauth/access_token'
+AUTHORIZATION_URL = 'http://twitter.com/oauth/authorize'
+SIGNIN_URL = 'http://twitter.com/oauth/authenticate'
+
+
+class OAuthApi(Api):
+    def __init__(self, consumer_key, consumer_secret, access_token=None):
+        if access_token:
+            Api.__init__(self,access_token.key, access_token.secret)
+        else:
+            Api.__init__(self)
+        self._Consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
+        self._signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
+        self._access_token = access_token
+
+
+    def _GetOpener(self):
+        opener = self._urllib.build_opener()
+        return opener
+
+    def _FetchUrl(self,
+                    url,
+                    post_data=None,
+                    parameters=None,
+                    no_cache=None):
+        '''Fetch a URL, optionally caching for a specified time.
+    
+        Args:
+          url: The URL to retrieve
+          post_data: 
+            A dict of (str, unicode) key/value pairs.  If set, POST will be used.
+          parameters:
+            A dict whose key/value pairs should encoded and added 
+            to the query string. [OPTIONAL]
+          no_cache: If true, overrides the cache on the current request
+    
+        Returns:
+          A string containing the body of the response.
+        '''
+        # Build the extra parameters dict
+        extra_params = {}
+        if self._default_params:
+          extra_params.update(self._default_params)
+        if parameters:
+          extra_params.update(parameters)
+    
+        # Add key/value parameters to the query string of the url
+        #url = self._BuildUrl(url, extra_params=extra_params)
+    
+        if post_data:
+            http_method = "POST"
+            extra_params.update(post_data)
+        else:
+            http_method = "GET"
+        
+        req = self._makeOAuthRequest(url, parameters=extra_params, 
+                                                    http_method=http_method)
+        self._signRequest(req, self._signature_method)
+
+        
+        # Get a url opener that can handle Oauth basic auth
+        opener = self._GetOpener()
+        
+        #encoded_post_data = self._EncodePostData(post_data)
+
+        if post_data:
+            encoded_post_data = req.to_postdata()
+            url = req.get_normalized_http_url()
+        else:
+            url = req.to_url()
+            encoded_post_data = ""
+            
+        no_cache=True
+        # Open and return the URL immediately if we're not going to cache
+        # OR we are posting data
+        if encoded_post_data or no_cache:
+          if encoded_post_data:
+              url_data = opener.open(url, encoded_post_data).read()
+          else:
+              url_data = opener.open(url).read()
+          opener.close()
+        else:
+          # Unique keys are a combination of the url and the username
+          if self._username:
+            key = self._username + ':' + url
+          else:
+            key = url
+    
+          # See if it has been cached before
+          last_cached = self._cache.GetCachedTime(key)
+    
+          # If the cached version is outdated then fetch another and store it
+          if not last_cached or time.time() >= last_cached + self._cache_timeout:
+            url_data = opener.open(url).read()
+            opener.close()
+            self._cache.Set(key, url_data)
+          else:
+            url_data = self._cache.Get(key)
+    
+        # Always return the latest version
+        return url_data
+    
+    def _makeOAuthRequest(self, url, token=None,
+                                        parameters=None, http_method="GET"):
+        '''Make a OAuth request from url and parameters
+        
+        Args:
+          url: The Url to use for creating OAuth Request
+          parameters:
+             The URL parameters
+          http_method:
+             The HTTP method to use
+        Returns:
+          A OAauthRequest object
+        '''
+        if not token:
+            token = self._access_token
+        request = oauth.OAuthRequest.from_consumer_and_token(
+                            self._Consumer, token=token, 
+                            http_url=url, parameters=parameters, 
+                            http_method=http_method)
+        return request
+
+    def _signRequest(self, req, signature_method=oauth.OAuthSignatureMethod_HMAC_SHA1()):
+        '''Sign a request
+        
+        Reminder: Created this function so incase
+        if I need to add anything to request before signing
+        
+        Args:
+          req: The OAuth request created via _makeOAuthRequest
+          signate_method:
+             The oauth signature method to use
+        '''
+        req.sign_request(signature_method, self._Consumer, self._access_token)
+    
+
+    def getAuthorizationURL(self, token, url=AUTHORIZATION_URL):
+        '''Create a signed authorization URL
+        
+        Returns:
+          A signed OAuthRequest authorization URL 
+        '''
+        req = self._makeOAuthRequest(url, token=token)
+        self._signRequest(req)
+        return req.to_url()
+
+    def getSigninURL(self, token, url=SIGNIN_URL):
+        '''Create a signed Sign-in URL
+        
+        Returns:
+          A signed OAuthRequest Sign-in URL 
+        '''
+        
+        signin_url = self.getAuthorizationURL(token, url)
+        return signin_url
+    
+    def getAccessToken(self, url=ACCESS_TOKEN_URL):
+        token = self._FetchUrl(url, no_cache=True)
+        return oauth.OAuthToken.from_string(token) 
+
+    def getRequestToken(self, url=REQUEST_TOKEN_URL):
+        '''Get a Request Token from Twitter
+        
+        Returns:
+          A OAuthToken object containing a request token
+        '''
+        resp = self._FetchUrl(url, no_cache=True)
+        token = oauth.OAuthToken.from_string(resp)
+        return token
+    
+    def GetUserInfo(self, url='https://twitter.com/account/verify_credentials.json'):
+        '''Get user information from twitter
+        
+        Returns:
+          Returns the twitter.User object
+        '''
+        json = self._FetchUrl(url)
+        data = simplejson.loads(json)
+        self._CheckForTwitterError(data)
+        return User.NewFromJsonDict(data)
+        

res/twibber_24x24.png

Added
New image

scripts/inno_setup.iss

 ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
 AppId={{C4008525-C8A4-4CD6-BFD7-0A9EFDD9F8A7}
 AppName=Twibber
-AppVerName=Twibber 0.0.3.5
-VersionInfoVersion=0.0.3.5
+AppVerName=Twibber 0.0.3.6
+VersionInfoVersion=0.0.3.6
 AppPublisher=Josh VanderLinden
 AppPublisherURL=http://bitbucket.org/codekoala/twitter-im
 AppSupportURL=http://bitbucket.org/codekoala/twitter-im
 MacOS X Usage:
     % python setup.py py2app
 
+The icons used in this app are not mine!
+TITLE:	Crystal Project Icons
+AUTHOR:	Everaldo Coelho
+SITE:	http://www.everaldo.com
+CONTACT: everaldo@everaldo.com
+
+Copyright (c)  2006-2007  Everaldo Coelho.
 """
 
 from distutils.core import setup
         'res/twibber.ico',
         'res/twibber.icns',
         'res/twibber_16x16.png',
+        'res/twibber_24x24.png',
         'res/twibber_32x32.png',
         'res/twibber_48x48.png',
         'res/twibber_128x128.png'
     twibber_target = Target(
         # used for the versioninfo resource
         description=description,
-        script="twibber.py",                                     # what to build
+        script="twibber.py",
         other_resources=[
             (RT_MANIFEST, 1, manifest_template % dict(prog=APP_TITLE))
         ],
 
 APP_TITLE = 'Twibber'
 __url__ = 'http://bitbucket.org/codekoala/twitter-im'
-__version__ = '0.0.3.5'
+__version__ = '0.0.3.6'
 
 if __name__ == '__main__':
     # configure logging
         info.Wrap(400)
         self.sizer.Add(info, 0)
 
-class TwitterAccountsPage(wizard.WizardPageSimple):
-    valid_users = {}
-
+class OAuthPage(wizard.WizardPageSimple):
+    authorized = False
     def __init__(self, parent):
         wizard.WizardPageSimple.__init__(self, parent)
-
-        # determine Twitter user information
-        for user in config.users.values():
-            self.valid_users[user.username] = user.password
-        usernames = self.valid_users.keys()
-        usernames.sort()
-
-        bold = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
-        bold.SetWeight(wx.BOLD)
+        self.parent = parent
+        self.user = None
 
         # build the GUI
-        self.sizer = make_page_title(self, 'Twitter Users')
-        info = wx.StaticText(self, label='Please configure the username and '
-            'password for each Twitter user whose updates you would like to '
-            'receive via instant message.  You may add more than one Twitter '
-            'user, but you must configure at least one.')
+        self.sizer = make_page_title(self, 'Twitter Authentication')
+        info = wx.StaticText(self, label='You must allow Twibber access to your'
+                'Twitter account in order to retrieve your updates.  In order '
+                'to better protect your privacy, Twibber uses only OAuth, which'
+                ' means that you don\'t have to enter your Twitter username and'
+                ' password here.')
         info.Wrap(400)
-        self.sizer.Add(info)
+        self.sizer.AddF(info, sf(wx.BOTTOM, 10))
+    
+        inst = wx.StaticText(self, label='Simply click the "Authorize Twibber" '
+                'button below.  This will open Twitter up in your web browser, '
+                'asking you to give Twibber read+write access to your '
+                'information. After you give Twibber access, come back to this '
+                'screen and click the "Finish Authorization" button.')
+        inst.Wrap(400)
+        self.sizer.Add(inst)
 
-        form_container = wx.BoxSizer(wx.HORIZONTAL)
-        self.sizer.AddF(form_container, sf(wx.TOP, 10))
+        buttons = wx.BoxSizer(wx.HORIZONTAL)
+        self.sizer.AddF(buttons, sf(wx.TOP, 10))
 
-        # user list, delete button
-        list_sizer = wx.BoxSizer(wx.VERTICAL)
-        form_container.AddF(list_sizer, sf(wx.RIGHT, 5))
+        self.authorize = wx.Button(self, label="Authorize Twibber")
+        buttons.Add(self.authorize)
 
-        self.user_list = wx.ListBox(self, choices=usernames)
-        list_sizer.AddF(self.user_list, sf(wx.BOTTOM, 10))
+        self.complete = wx.Button(self, label="Finish Authorization")
+        self.complete.Enable(False)
+        buttons.Add(self.complete)
 
-        self.delete = wx.Button(self, label='Delete User')
-        self.delete.Enable(False)
-        list_sizer.AddF(self.delete, wx.SizerFlags(wx.ALIGN_CENTER_HORIZONTAL))
+        self.authorize.Bind(wx.EVT_BUTTON, self.BeginAuthorization)
+        self.complete.Bind(wx.EVT_BUTTON, self.FinishAuthorization)
 
-        # Username/Password Form
-        form = wx.FlexGridSizer(rows=3, cols=2, vgap=10, hgap=10)
-        form_container.AddF(form, sf(wx.LEFT, 5))
+    def BeginAuthorization(self, evt=None):
+        """
+        Sends the user to Twitter to authorize Twibber to access their account
+        """
+        self.user = User()
+        self.user.BeginOAuth()
+        self.authorize.Enable(False)
+        self.complete.Enable(True)
 
-        cv = wx.SizerFlags(wx.ALIGN_CENTER_VERTICAL)
-        twitter_id = wx.StaticText(self, label='Twitter Username:')
-        twitter_id.SetFont(bold)
-        form.AddF(twitter_id, cv)
-        self.username = wx.TextCtrl(self)
-        form.AddF(self.username, sf(wx.ALL, 0))
+    def FinishAuthorization(self, evt=None):
+        self.user.CompleteOAuth()
+        try:
+            log.info('Attempting to authenticate using OAuth')
+            self.user.api._FetchUrl('http://twitter.com/account/verify_credentials.json')
+            log.info('OAuth authorization succeeded!')
+            self.authorized = True
+            self.parent.ShowPage(self.parent.page2)
+        except urllib2.HTTPError:
+            log.info('OAuth authorization failed.')
+            wx.MessageBox('OAuth authorization process failed.  Please try '
+                'again.', 'Failed Authorization', style=wx.ICON_ERROR)
 
-        twitter_pass = wx.StaticText(self, label='Twitter Password:')
-        twitter_pass.SetFont(bold)
-        form.AddF(twitter_pass, cv)
-        self.password = wx.TextCtrl(self, style=wx.PASSWORD)
-        form.AddF(self.password, sf(wx.ALL, 0))
-
-        form.Add(wx.StaticText(self, label=''))
-        self.save = wx.Button(self, label='Save User')
-        self.save.Enable(False)
-        form.AddF(self.save, sf(wx.ALL, 0))
-
-        # bind to some events
-        self.Bind(wx.EVT_LISTBOX, self.ToggleDeleteButton, self.user_list)
-        self.Bind(wx.EVT_LISTBOX_DCLICK,
-                  self.EditUserInfo, self.user_list)
-        self.delete.Bind(wx.EVT_BUTTON, self.DeleteSelectedUser)
-        self.username.Bind(wx.EVT_TEXT, self.ToggleSaveButton)
-        self.password.Bind(wx.EVT_TEXT, self.ToggleSaveButton)
-        self.save.Bind(wx.EVT_BUTTON, self.CheckTwitterUser)
-
-    def CheckTwitterUser(self, evt=None):
-        """
-        Verifies that the user can log into Twitter using the specified
-        username and password
-        """
-        username = self.username.GetValue()
-        password = self.password.GetValue()
-        if len(username) and len(password):
-            try:
-                api = twitter.Api(username=username, password=password)
-                api.GetFollowers()
-            except urllib2.HTTPError:
-                wx.MessageBox('Failed to authenicate with Twitter!',
-                              'Invalid Credentials')
-            else:
-                self.valid_users[username] = password
-                self.username.Clear()
-                self.password.Clear()
-                self.UpdateUserList()
-
-    def DeleteSelectedUser(self, evt=None):
-        """
-        Removes the selected user(s) from the list of Twitter users
-        """
-        for index in self.user_list.GetSelections():
-            username = self.user_list.GetString(index)
-
-            res = wx.MessageBox('Are you sure you want to remove %s from the '
-                'list of Twitter users?' % username, 'Confirm Delete',
-                style=wx.YES_NO|wx.NO_DEFAULT|wx.ICON_QUESTION)
-            if res == wx.YES:
-                del self.valid_users[username]
-                self.UpdateUserList()
-
-    def EditUserInfo(self, evt=None):
-        """
-        Pulls the username and password for the selected user out so it can be
-        updated
-        """
-        user = self.user_list.GetStringSelection()
-        self.username.SetValue(user)
-        self.password.SetValue(self.valid_users[user])
-
-    def ToggleDeleteButton(self, evt=None):
-        """
-        Toggles the Delete User button depending on whether or not a user is
-        selected.
-        """
-        self.delete.Enable(len(self.user_list.GetStringSelection()))
-
-    def ToggleSaveButton(self, evt=None):
-        """
-        Toggles the Save User button based on the input provided by the user
-        """
-        username = self.username.GetValue()
-        password = self.password.GetValue()
-        self.save.Enable(len(username.strip()) and len(password.strip()))
-
-    def UpdateUserList(self):
-        """
-        Refreshes the list of Twitter users
-        """
-        usernames = self.valid_users.keys()
-        usernames.sort()
-        self.user_list.SetItems(usernames)
-
-    def ValidatePage(self, evt):
-        """
-        Verifies that at least one Twitter account has been successfully
-        configured.
-        """
-        if len(self.valid_users) <= 0:
-            wx.MessageBox('Please specify at least one Twitter user!',
-                          'Incomplete Data', style=wx.ICON_ERROR)
-            evt.Veto()
+    def ValidatePage(self, *args, **kwargs):
+        if not self.authorized:
+            self.authorize.Enable(True)
+            self.complete.Enable(False)
+        return self.authorized
 
 class RecipientPage(wizard.WizardPageSimple):
     def __init__(self, parent):
         # - configure special Jabber account
 
         self.page0 = WelcomePage(self)
-        self.page1 = TwitterAccountsPage(self)
+        self.page1 = OAuthPage(self)
         self.page2 = RecipientPage(self)
         self.page3 = TransportPage(self)
 
         """
         log.info('Persisting configuration to disk')
 
-        # save Twitter users
-        existing = dict((u.username, u.password) for u in config.users.values())
-        current_users = {}
-        for username, password in self.page1.valid_users.items():
-            if username in existing.keys():
-                log.info(username + ' already exists... updating password')
-                # update the password
-                for user in config.users.values():
-                    if user.username == username:
-                        user.password = password
-            else:
-                log.info('adding %s to configuration' % username)
-                user = User(username, password)
-            current_users[username] = user
-        config.users = current_users
+        user = self.page1.user
+        config._users = {user.username: user}
 
         config.send_to_user = self.page2.username.GetValue()
         config.login_as_user = self.page3.jabber_id.GetValue()
         config.login_as_pass = self.page3.password.GetValue()
-
         config.persist()
 
         # make sure we enable or disable the "as" command if the user changed
         # the number of Twitter users
-        self.parent.twibber.InitCommands()
+        self.parent.twibber.OnConfigChanged()
 
 class TwitterTray(wx.TaskBarIcon):
     """
             if sys.platform in ('win32', 'nt'):
                 icon = wx.Icon('res/twibber.ico', wx.BITMAP_TYPE_ICO)
             else:
-                icon = wx.Icon('res/twibber_32x32.png', wx.BITMAP_TYPE_PNG)
+                icon = wx.Icon('res/twibber_24x24.png', wx.BITMAP_TYPE_PNG)
             self.SetIcon(icon, '%s v%s' % (APP_TITLE, __version__))
 
         self.menu = None
         self.tray.Bind(wx.EVT_CLOSE, self.OnExit)
         self.tray.Bind(wx.EVT_MENU, self.OnMenu)
 
-        self.twibber.BeginBeingUseful()
+        self.twibber.BeginBeingUseful(self)
 
     def OnConfiguration(self, evt=None):
         """
         # force a reconnect to Jabber
         self.twibber._client = None
 
-    def OnExit(self, evt=None):
+    def OnExit(self, evt=None, from_core=False):
         """
         Handles the shutdown proceedure for the app
         """
         except:
             # don't bother with dead objects :D
             pass
-        
-        self.twibber.OnExit()
+
+        if not from_core:
+            self.twibber.OnExit(from_gui=True)
 
     def OnMenu(self, evt):
         """
+def timeout(func, args=(), kwargs={}, timeout_duration=10, default=None):
+    """
+    This function will spawn a thread and run the given function
+    using the args, kwargs and return the given default value if the
+    timeout_duration is exceeded.
+
+    Ganked from: http://2ze.us/Zp
+    """
+    import threading
+    class InterruptableThread(threading.Thread):
+        def __init__(self):
+            threading.Thread.__init__(self)
+            self.result = default
+        def run(self):
+            self.result = func(*args, **kwargs)
+    it = InterruptableThread()
+    it.start()
+    it.join(timeout_duration)
+
+    if it.isAlive():
+        return it.result
+    else:
+        return it.result