Commits

Josh VanderLinden committed 33b7283

Renamed the app (again! Twim is already a Twitter app for blackberry) to Twibber. There's already a Twibber project out there that I found, but it had nothing to do with Twitter, so I'm going to take my chances. I abandoned the idea of using SQLAlchemy and a SQLite backend. There were too many thread concurrency issues to bother with. A simple configuration file work perfectly fine. Now all tweets from any given session will be cached in memory. This provides a mechanism that allows easier access to commands like retweeting and replying. I also made it easier to run the app without the need for the GUI. When you receive a tweet, any links in the mesasge will be checked briefly. If the link's destination has a domain other than the domain of the original URL, the real domain will be placed beside the mask.

Comments (0)

Files changed (22)

MANIFEST

-README
-LICENSE
-setup.py
-__init__.py
-twim.py
-twim.ico
+include README LICENSE res/* *.py
 import urllib2
 import xmpp
 from datetime import datetime, timedelta
-from db import Cache, Config
+from data import cache, config
 from threading import Event, Thread
 
-log = logging.getLogger('twim.core')
+log = logging.getLogger('twibber.core')
 
 AT_REPLY_RE = re.compile('@(\w+)')
 HASH_TAG_RE = re.compile('([^&]?)#([\w\-]+)([^;]?)')
     0x2019: u"'",
 }
 
-class Worker(Thread):
-    def __init__(self, twim):
-        Thread.__init__(self)
-        self.twim = twim
-
-class TwimApp(object):
+class Twibber(object):
     """
     The main program
     """
     _client = None
     _commands = {}
     _history = []
-
-    # these are here to make it a tiny bit easier to deal with threading issues
-    config = property(lambda t: Config())
-    cache = property(lambda t: Cache())
+    _queue = []
 
     def __init__(self, with_gui=True):
         log.info('Starting app on %s' % sys.platform)
-        self.interval = timedelta(seconds=self.config.update_interval)
+        self.interval = timedelta(seconds=config.update_interval)
 
         # start the GUI if necessary
         if with_gui:
         """
         log.info('Performing post-init startup')
         self.InitCommands()
-
-        try:
-            # handle messages sent by the user
-            self.client.RegisterHandler('message', self.OnMessage)
-
-            # tell people we're online
-            self.client.sendInitPresence()
-            self.presence = xmpp.Presence(priority=5, show='chat')
-            self.client.send(self.presence)
-        except Exception, ex:
-            sys.exit(ex)
+        self.Connect()
 
         # create some threads so we don't have an unresponsive program
         log.info('starting threads')
             'trends': self.OnTrendsCommand,
             'message': self.OnDirectMessageCommand,
             'dm': self.OnDirectMessageCommand,
-            'limits': self.OnRateLimitsCommand
+            'limits': self.OnRateLimitsCommand,
+            'quit': self.OnQuitCommand
         }
 
         # enable the "as" command if we have multiple users setup
-        if len(self.config.users) > 1:
+        if len(config.users) > 1:
             self._commands['as'] = self.OnAsCommand
 
+    def Connect(self):
+        """
+        Attempts to establish a connection with Jabber
+        """
+
+        try:
+            # handle messages sent by the user
+            self.client.RegisterHandler('message', self.OnMessage)
+
+            # tell people we're online
+            self.client.sendInitPresence()
+            self.presence = xmpp.Presence(priority=5, show='chat')
+            self.client.send(self.presence)
+        except Exception, ex:
+            log.error(ex)
+            sys.exit(ex)
+
     def GetClient(self):
         """
         Gets the client and connects to Jabber if we're not already connected
         """
-        config = self.config
         try:
             if not self._client:
                 # try to connect to Jabber
-                self.jid = xmpp.protocol.JID(config.login_as_user)
+                self.jid = xmpp.JID(config.login_as_user)
                 self._client = xmpp.Client(self.jid.getDomain(), debug=[])
 
             if not self._client.isConnected():
-                self.conn = self._client.connect()
+                conn = self._client.connect()
 
-                if self.conn:
-                    self.auth = self._client.auth(self.jid.getNode(),
-                                                  config.login_as_pass,
-                                                  resource=self.jid.getResource())
-                    if not self.auth:
-                        raise Exception('Failed to log into Jabber')
-                else:
+                if not conn:
                     raise Exception('Failed to connect')
+                auth = self._client.auth(self.jid.getNode(),
+                                              config.login_as_pass,
+                                              resource=self.jid.getResource())
+                if not auth:
+                    raise Exception('Failed to log into Jabber')
         except Exception, ex:
             log.error(ex)
+            self._client = None
             time.sleep(5)
             return self.GetClient()
 
         """
         Shut everything down
         """
+        config.persist()
+
         log.info('Terminating the application')
         self.checker.set()
         self.poster.set()
         if I wanted to filter all tweets dealing with #io2009, I would do
         `./filter io2009` (no hash)
         """
-        config = self.config
 
         if len(remaining.strip()) == 0:
-            filtered = '\n'.join(['#' + t.tag for t in config.get_filtered_tags()])
+            filtered = '\n'.join(['#' + tag for tag in config.filtered_tags])
             self.TellUser('Currently filtered tags:\n' + filtered)
         else:
             # add a new tag to the filter list
             if args:
                 tag = args.group(1)
                 try:
-                    config.add_filtered_tag(tag)
+                    config._filtered.append(tag)
                     self.TellUser('#%s is now filtered. Type "./filter" to see '
                     'a list of filtered tags or "./unfilter [tag]" to remove a '
                     'tag that is currently filtered.' % tag)
         list of hashtags to filter, I could remove it by typing
         `./unfilter io2009`
         """
-        config = self.config
 
         # remove a new tag from the filter list
         args = re.search('^([^ $]+)', remaining)
         if args:
             tag = args.group(1)
             try:
-                config.remove_filtered_tag(tag)
+                config._filtered.remove(tag)
                 self.TellUser('#%s has been removed!' % tag)
             except:
                 self.TellUser('#%s is not filtered' % tag)
                 info = user.api.GetUser(username)
 
                 init = ('location', 'description', 'url', 'utc_offset',
-                        'time_zone', 'name')
+                        'time_zone', 'name', 'friends_count', 'status',
+                        'profile', 'statuses_count', 'followers_count')
                 info_dict = dict((k, 'not specified') for k in init)
                 info_dict['profile'] = 'http://twitter.com/%s' % username
                 info_dict['status_text'] = info.status.text
     def OnUndoCommand(self, user, body, remaining):
         """
         The undo command allows you to undo certain actions that you have done
-        using Twim.  If you accidentally post a update to Twitter, you can just
-        type ./undo and it will be removed (assuming you want to remove the
-        last tweet you posted with Twim).
+        using Twibber.  If you accidentally post a update to Twitter, you can
+        just type ./undo and it will be removed (assuming you want to remove the
+        last tweet you posted with Twibber).
         """
         if len(self._history):
             last_tweet = self._history.pop()
         else:
             self.TellUser('There is nothing left for you to undo right now.')
 
-    def OnSearchCommand(self, user, body, remaining, callback=None):
+    def OnSearchCommand(self, user, body, remaining):
         """
         Allows you to quickly search for something on Twitter.
 
                 'page': params['p'],
                 'rpp': params['pp']
             }
-            url = 'http://search.twitter.com/search.json'
+            vars = urllib.urlencode(query)
+            url = 'http://search.twitter.com/search.json?' + vars
 
-            log.info('[url=%s, params=%s and %s]' % (url, query, params))
+            log.info('Searching url=' + url)
             try:
-                json = user.api._FetchUrl(url, parameters=query)
+                self.TellUser('Searching for "%s"... please wait.' % remaining)
+                json = urllib2.urlopen(url).read()
                 data = simplejson.loads(json)
                 results = data.get('results', [])
                 tweets = []
                     tweet = twitter.Status.NewFromJsonDict(result)
                     tweet.user = user.api.GetUser(result['from_user'])
                     tweets.append(tweet)
-
-                # this is used for the OnIDsCommand
-                if callback:
-                    callback(tweets)
-
                 log.debug(tweets)
 
                 if len(tweets):
         ./schedule 18:30 This should be posted at 6:30 PM
         ./schedule 15minutes This should be posted 15 minutes from now
         """
-        config = self.config
 
         params = (
             ('year', 'month', 'day', 'hour', 'minute', 'tweet'),
                     schedule_at = datetime(**args)
 
                 log.info('Scheduled to say "%s" at %s' % (tweet, schedule_at))
-                config.add_scheduled_tweet.append(schedule_at, user.username,
-                                                  tweet, is_reminder)
+                config.schedule(user.username, tweet, schedule_at, is_reminder)
 
                 action = is_reminder and 'remind you of' or 'tweet'
                 self.TellUser('I am now scheduled to %s "%s" at %s' % (
                 self.TellUser(
                     "Sorry, but I don't understand your scheduling format!")
             else:
-                schedule = [st for st in config.get_scheduled_tweets()]
+                schedule = [st for st in config.scheduled_tweets]
                 self.TellUser('Scheduled Tweets:\n' + '\n'.join(schedule))
 
     def OnRemindCommand(self, user, body, remaining):
         The retweet command automatically prepends your tweet with "RT @[from]"
         where "[from]" is replaced by the person whose tweet you're retweeting.
         """
-        cache = self.cache
 
-        args = re.search('^(\d+)', remaining)
+        args = re.search('^(\d+)(.*)$', remaining)
         if args:
             tid = args.group(1)
-            tweet = cache.with_id(tid)
+            comments = args.group(2).strip()
+            tweet = cache.by_id(tid)
             if tweet:
-                text = 'RT @%s %s' % (tweet.user.screen_name, tweet.text)
+                text = tweet.text
+                
+                # make it possible to change the retweet
+                if len(comments) >= len(tweet.text):
+                    text = comments
+                    comments = ''
+                elif len(comments):
+                    # prepend a space for good taste
+                    text += ' ' + comments
+
+                text = 'RT @%s %s' % (tweet.user.screen_name, text)
                 self.PostUpdate(user, text)
             else:
                 self.TellUser('Invalid tweet!  Please try again.')
 
     def OnReplyCommand(self, user, body, remaining):
         """
-        The reply command allows you to reply to a particular tweet.  You must
-        first run the ./ids command to get an ID for the tweet you wish to
-        reply to.  For example, if you wanted to reply to something that @ev
-        said about Maui, you would do this:
+        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:
 
-        ./ids from:ev Maui
         ev: [ 0 ] In Maui. Wowee.
-        ./reply 0
+        ./reply 0 I wish I were in Maui
 
-        The reply command automatically adds the @[from], where "[from]" is
+        The reply command automatically adds the @[to], where "[to]" is
         replaced by the username of the user to whom you are replying.
         """
-        cache = self.cache
 
         args = re.search('^(\d+) (.*)$', remaining)
         if args:
             tid = args.group(1)
             reply = args.group(2)
-            tweet = cache.with_id(tid)
+            tweet = cache.by_id(tid)
             if tweet:
                 text = '@%s %s' % (tweet.user.screen_name, reply)
                 self.PostUpdate(user, text, in_reply_to_status_id=tweet.id)
     def OnFavoriteCommand(self, user, body, remaining):
         """
         The favorite command allows you to mark a particular tweet as one of
-        your favorite.  You must first run the ./ids command to get an ID for
-        the tweet you wish to favorite.  For example, if you wanted to mark
-        something that @ev said about Maui, you would do:
+        your favorite.  For example, if you wanted to mark something that @ev 
+        said about Maui, you would do:
 
-        ./ids from:ev Maui
         ev: [ 0 ] In Maui. Wowee.
         ./favorite 0
         """
-        cache = self.cache
 
         args = re.search('^(\d+)', remaining)
         if args:
             tid = args.group(1)
-            tweet = cache.with_id(tid)
+            tweet = cache.by_id(tid)
             if tweet:
                 try:
                     user.api.CreateFavorite(tweet)
     def OnUnfavoriteCommand(self, user, body, remaining):
         """
         The unfavorite command allows you to remove a particular tweet as one
-        of your favorite.  You must first run the ./ids command to get an ID
-        for the tweet you wish to unfavorite.  For example, if you wanted to
-        unfavorite something that @ev said about Maui, you would do:
+        of your favorite.  For example, if you wanted to unfavorite something 
+        that @ev said about Maui, you would do:
 
-        ./ids from:ev Maui
         ev: [ 0 ] In Maui. Wowee.
         ./unfavorite 0
         """
-        cache = self.cache
 
         args = re.search('^(\d+)', remaining)
         if args:
             tid = args.group(1)
-            tweet = cache.with_id(tid)
+            tweet = cache.by_id(tid)
             if tweet:
                 try:
                     user.api.DestroyFavorite(tweet)
             self.TellUser(
                 'Something went wrong when I tried to get your limits.')
 
+    def OnQuitCommand(self, user, body, remaining):
+        """
+        The quit command will remotely shut down Twibber.
+        """
+        self.OnExit()
+
     def FindCommand(self, text):
         """
-        Attempts to find a Twim command based on the specified text.  The text
-        may be a full command or the prefix that returns a unique command.
+        Attempts to find a Twibber command based on the specified text.  The
+        text may be a full command or the prefix that returns a unique command.
         """
         command = None
         if text in self._commands.keys():
 
         return command
 
-    def OnMessage(self, con, evt):
+    def OnMessage(self, client, evt):
         """
         Handles messages sent by the user.  This will post updates to Twitter
         directly from your IM client.
         """
-        config = self.config
         pattern = '^\.\/'
         sender = evt.getFrom().getStripped()
         body = evt.getBody()
                 self.client.send(xmpp.Presence(to=config.send_to_user, typ=typ))
             return
 
-        as_user = config.GetDefaultUser()
+        as_user = config.default_user
         body = body.strip()
         if body.startswith('./'):
             # find the commands provided by the user
         """
         For the love of threads!
         """
-        config = self.config
         while not event.isSet():
             try:
                 event.wait(interval)
-
-                # run through any scheduled tweets to see if it's time to post
-                for tweet in config.get_due_tweets():
-                    log.info('Time to tweet "%s"' % tweet.text)
-                    self.PostUpdate(tweet.user, tweet.text)
-                    config.remove_scheduled_tweet(tweet)
-
                 action()
             except Exception, ex:
                 log.error(ex)
         """
         try:
             now = datetime.now()
-            for user in self.config.users:
+            for user in config.users.values():
                 last_update = user.last_update or now - self.interval
                 next_run = last_update + self.interval
                 if next_run <= now:
+                    log.info('Getting updates for %s' % user.username)
                     self.GetUpdatesFor(user)
         except urllib2.HTTPError, ex:
             log.error(ex)
         Handles messages sent through Jabber... this is running in its own
         thread
         """
-        print 'Checking for posts'
+
+        # 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)
+            config._scheduled.remove(tweet)
+
         self.client.Process(1)
 
     def GetUpdatesFor(self, user):
         Connects to Twitter to find updates for a particular user.  If updates
         are found, they're IM'ed out.
         """
-        config = self.config
 
         # get some updates
         last_id = user.last_tweet_id or config.last_tweet_id
         """
         Sends a collection of tweets to the user's IM
         """
-        config = self.config
-        cache = self.cache
         prefix = len(config.users) > 1 and '(%s) ' % user.username or ''
 
         for tweet in tweets:
-            tweet = cache.get(tweet)
             name = tweet.user and tweet.user.screen_name or ''
 
+            # make sure this tweet is in the cache
+            cache.set(tweet)
+
             # make sure the tweet doesn't have any filtered tags in it
             good_tags = True
-            for tag in config.get_filtered_tags():
-                if '#' + tag.tag.lower() in tweet.text.lower():
+            for tag in config.filtered_tags:
+                if '#' + tag.lower() in tweet.text.lower():
                     good_tags = False
                     break
             if not good_tags: continue
             text = self.HTMLizeTweet(name, tweet)
             log.debug('HTML Text: ' + text)
 
-            pre = '[ %i ] %s' % (tweet.counter, prefix)
+            pre = '[ %i ] %s' % (cache.get_id(tweet.id), prefix)
             params = {
-                'body': pre + name + ': ' + tweet.text,
+                'body': pre + name + ': ' + self.ShowLinkDestinations(tweet.text),
                 'html': pre + text
             }
             output = clean(TWEET % params)
             if not do_not_update:
                 config.last_tweet_id = user.last_tweet_id = int(tweet.id)
 
+    def ShowLinkDestinations(self, text):
+        """
+        This will search through the specified text, looking for anything that
+        resembles a URL.  When it finds one, it will go out to the Internet and
+        find the real domain for the destination site.  The domain will be
+        inserted into the text if it differs from the domain in the original URL
+        """
+
+        # TODO make this optional
+        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)
+            except Exception, ex:
+                # it's not that important...
+                log.error(ex)
+        return text
+
     def HTMLizeTweet(self, name, tweet):
         """
         Makes a few enhancements to the text of a tweet so it can render as
             uhref = ''
 
         text = clean(tweet.text.replace('& ', '&amp; '))
-        text = AT_REPLY_RE.sub(
-            r'@<a href="http://twitter.com/\1">\1</a>',
-            text)
+        text = self.ShowLinkDestinations(text)
+        at_users = AT_REPLY_RE.findall(text)
+        template = '<span%(c)s>@<a href="http://twitter.com/%(u)s"%(c)s>%(u)s</a></span>'
+
+        # gets rid of duplicates
+        for username in dict.fromkeys(at_users).keys():
+            n = {'u': username, 'c': ''}
+            if username in config.users.keys():
+                n['c'] = ' style="font-weight:bold;color:red;font-style:italic;"'
+            text = text.replace('@' + username, template % n)
         text = HASH_TAG_RE.sub(
             r'\1<em><a href="http://twitter.com/#search?q=%23\2">#\2</a></em>\3',
             text)
         """
         Sends a message back to the user
         """
-        msg = xmpp.Message(to=self.config.send_to_user,
+        msg = xmpp.Message(to=config.send_to_user,
                            body='==> ' + message)
         self.client.send(msg)
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+This is where all of the configuration and cache storage takes place.  Two
+separate sqlite databases are used here: a persistent one for configuration,
+and a volatile one for caching.  Maintaining a separate database for cache
+makes it easier to use commands like retweeting and replying.
+"""
+
+import base64
+import logging
+import os
+import pickle
+import twitter
+from ConfigParser import SafeConfigParser
+from datetime import datetime
+
+log = logging.getLogger('twibber.db')
+
+HOME_DIR = os.path.expanduser('~')
+CONFIG = os.path.join(HOME_DIR, '.twibberrc')
+
+class User(object):
+    """
+    Represents a Twitter user in Twibber.
+    """
+    __slots__ = ('username', '_password', 'last_tweet_id', 'last_update', '_api')
+
+    def __init__(self, username, 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
+
+    def __unicode__(self):
+        return self.username
+
+    def _get_api(self):
+        """
+        Returns the Twitter API for this user
+        """
+        if not self._api:
+            import twibber
+            # connect to Twitter for this user
+            self._api = twitter.Api(username=self.username,
+                                    password=self.password)
+            self._api.SetSource(twibber.APP_TITLE)
+            self._api.SetXTwitterHeaders(twibber.APP_TITLE,
+                                         twibber.__url__,
+                                         twibber.__version__)
+        return self._api
+    api = property(_get_api)
+
+    def _get_password(self):
+        return base64.b64decode(self._password)
+    def _set_password(self, value):
+        self._password = base64.b64encode(value)
+    password = property(_get_password, _set_password)
+
+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
+        self.send_at = send_at
+        self.is_reminder = is_reminder
+
+    def __unicode__(self):
+        return u'On %s: %s' % (self.send_at.strftime('%Y-%m-%d (%H:%M)'),
+                               self.text)
+
+class Config(object):
+    Section = 'general'
+    Defaults = {
+        'update_interval': (90, int),
+        'last_tweet_id': (0, int),
+        'login_as_user': '',
+        'login_as_pass': '',
+        'send_to_user': '',
+        'twitter_users': '',
+        'default_user': '',
+        'filtered_tags': '',
+        'scheduled_tweets': ''
+    }
+    _users = None
+    _scheduled = None
+    _filtered = None
+    _default_user = None
+
+    def __init__(self, configfile):
+        self.configfile = configfile
+
+        # 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')
+
+        log.debug('Reading configuration from: %s' % self.configfile)
+        self.parser = SafeConfigParser()
+        self.parser.read(self.configfile)
+
+        # make sure we have the file setup properly
+        if not self.parser.has_section(Config.Section):
+            self.parser.add_section(Config.Section)
+
+        for option in Config.Defaults.keys():
+            if not self.parser.has_option(Config.Section, option):
+                self.set(option, self.get_default(option))
+
+        # save the configuration just in case the file has not yet been created
+#        self.persist()
+
+        for key in self.to_grab:
+            setattr(self, key, self.get(key))
+
+    def get(self, option):
+        """
+        Retrieves a simple configuration value from the config file
+        """
+        try:
+            cast = Config.Defaults[option][1]
+        except IndexError:
+            cast = str
+
+        # determine which method we'll use to retrieve the option based
+        # on its datatype
+        func = {
+            bool: self.parser.getboolean,
+            float: self.parser.getfloat,
+            int: self.parser.getint
+        }.get(cast, self.parser.get)
+
+        value = func(Config.Section, option)
+
+        if value == None:
+            value = self.get_default(option)
+
+        return value
+
+    def set(self, option, value):
+        """
+        Updates the configuration file with some value
+        """
+        self.parser.set(Config.Section, option, str(value))
+
+    def get_default(self, option):
+        """
+        Determines the default value of some setting
+        """
+        default = Config.Defaults.get(option, '')
+        if isinstance(default, tuple):
+            default = default[0]
+        return default
+
+    def _get_users(self):
+        """
+        Retrieves the information for the Twitter accounts that the user wishes
+        to check
+        """
+        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
+        return self._users
+
+    def _set_users(self, users):
+        """
+        Makes sure that a particular list of users is in our database
+        """
+        assert isinstance(users, dict)
+
+        kvp = ['%s:%s' % (u.username, u._password) for u in users.values()]
+        self.set('twitter_users', '|'.join(kvp))
+    users = property(_get_users, _set_users)
+
+    def _get_filtered_tags(self):
+        """
+        Retrieves any tags that should be filtered
+        """
+        if not self._filtered:
+            self._filtered = self.get('filtered_tags').split(',')
+            if '' in self._filtered:
+                self._filtered.remove('')
+        return self._filtered
+    
+    def _set_filtered_tags(self, tags):
+        """
+        Sets the value for the filtered tags
+        """
+        self.set('filtered_tags', ','.join(tags))
+    filtered_tags = property(_get_filtered_tags, _set_filtered_tags)
+
+    def _get_due_tweets(self):
+        """
+        Retrieves any scheduled tweets that are past due
+        """
+        now = datetime.now()
+        return [t for t in self.scheduled_tweets if t.send_at <= now]
+    due_tweets = property(_get_due_tweets)
+
+    def _get_scheduled_tweets(self):
+        """
+        Retrieves all scheduled tweets
+        """
+        if not self._scheduled:
+            b64 = self.get('scheduled_tweets')
+            if b64 and len(b64):
+                self._scheduled = pickle.loads(base64.b64decode(b64))
+
+            if not isinstance(self._scheduled, list):
+                self._scheduled = []
+        return self._scheduled
+
+    def _set_scheduled_tweets(self, tweets):
+        """
+        Sets the value of the scheduled tweets in the config file
+        """
+        encoded = base64.b64encode(pickle.dumps(tweets))
+        self.set('scheduled_tweets', encoded)
+    scheduled_tweets = property(_get_scheduled_tweets, _set_scheduled_tweets)
+
+    def schedule(self, user, text, send_at, is_reminder):
+        """
+        Appends a ScheduledTweet object to our list
+        """
+        self._scheduled.append(ScheduledTweet(user, text, send_at, is_reminder))
+
+    def _get_default_user(self):
+        """
+        Returns the default Twitter user
+        """
+        if not self._default_user:
+            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
+
+    def _set_default_user(self, user):
+        """
+        Sets the default user's username in the configuration file
+        """
+        user = self.default_user
+        if user:
+            self.set('default_user', user.username)
+    default_user = property(_get_default_user, _set_default_user)
+
+    def persist(self):
+        """
+        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()
+
+class Cache(object):
+    _tweets = {}
+    _counter_map = {}
+    _tweet_map = {}
+    _counter = 1
+
+    def set(self, tweet):
+        """
+        Ensures that a tweet is in the cache
+        """
+        if not self._tweets.has_key(tweet.id):
+            self._tweets[tweet.id] = tweet
+            self._counter_map[self._counter] = tweet.id
+            self._tweet_map[tweet.id] = self._counter
+            self._counter += 1
+        return self._tweets[tweet.id]
+
+    def by_id(self, id):
+        """
+        Finds the tweet which corresponds to the specified cache ID
+        """
+        return self._tweets.get(self._counter_map.get(int(id), None), None)
+
+    def get_id(self, id):
+        """
+        Finds the cache ID for the specified tweet ID
+        """
+        return self._tweet_map.get(int(id), None)
+
+config = Config(CONFIG)
+cache = Cache()

db.py

-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-This is where all of the configuration and cache storage takes place.  Two
-separate sqlite databases are used here: a persistent one for configuration,
-and a volatile one for caching.  Maintaining a separate database for cache
-makes it easier to use commands like retweeting and replying.
-"""
-
-import logging
-import os
-import sqlalchemy as sql
-import sqlite3
-import twitter
-from datetime import datetime
-from functools import wraps
-from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import sessionmaker, scoped_session
-from sqlalchemy import pool
-
-log = logging.getLogger('twim.db')
-
-HOME_DIR = os.path.expanduser('~')
-CONFIG = os.path.join(HOME_DIR, '.twimdb')
-CACHE = os.path.join(HOME_DIR, '.twimcache')
-
-ConfigBase = declarative_base()
-CacheBase = declarative_base()
-
-ConfigSession = scoped_session(sessionmaker())
-CacheSession = scoped_session(sessionmaker())
-
-class TwimUser(ConfigBase):
-    """
-    Represents a Twitter user in Twim.
-    """
-    __tablename__ = 'twim_user'
-    _api = None
-
-    username = sql.Column(sql.String, primary_key=True)
-    password = sql.Column(sql.String)
-    last_tweet_id = sql.Column(sql.Integer)
-    last_update = sql.Column(sql.DateTime)
-
-    def __init__(self, username, password, last_tweet=None, last_update=None):
-        self.username = username
-        self.password = password
-        self.last_tweet = last_tweet
-        self.last_update = last_update
-
-    def __unicode__(self):
-        return self.username
-
-    def _get_api(self):
-        """
-        Returns the Twitter API for this user
-        """
-        if not self._api:
-            import twim
-            # connect to Twitter for this user
-            self._api = twitter.Api(username=self.username,
-                                    password=self.password)
-            self._api.SetSource(twim.APP_TITLE)
-            self._api.SetXTwitterHeaders(twim.APP_TITLE,
-                                         twim.__url__,
-                                         twim.__version__)
-        return self._api
-    api = property(_get_api)
-
-class Setting(ConfigBase):
-    """
-    Represents a configuration setting for Twim
-    """
-    __tablename__ = 'setting'
-
-    key = sql.Column(sql.String, primary_key=True)
-    value = sql.Column(sql.String)
-    default = sql.Column(sql.String)
-    data_type = sql.Column(sql.String)
-
-    def __init__(self, key, default='', data_type='str'):
-        self.key = key
-        self.default = default
-        self.data_type = data_type
-
-    def __unicode__(self):
-        self.key
-
-class ScheduledTweet(ConfigBase):
-    """
-    Represents a tweet that is scheduled to be sent at some point in the future
-    """
-    __tablename__ = 'scheduled_tweet'
-
-    id = sql.Column(sql.Integer, primary_key=True)
-    as_user = sql.Column(sql.String, sql.ForeignKey('twim_user.username'))
-    text = sql.Column(sql.String)
-    send_at = sql.Column(sql.DateTime)
-    is_reminder = sql.Column(sql.Boolean)
-
-    def __init__(self, user, text, send_at, is_reminder=False):
-        self.as_user = user
-        self.text = text
-        self.send_at = send_at
-        self.is_reminder = is_reminder
-
-    def __unicode__(self):
-        return u'On %s: %s' % (self.send_at.strftime('%Y-%m-%d (%H:%M)'),
-                               self.text)
-
-class FilteredTag(ConfigBase):
-    """
-    Represents a hashtag that the user would like to filter from the updates
-    sent to them
-    """
-    __tablename__ = 'filtered_tag'
-
-    tag = sql.Column(sql.String, primary_key=True)
-
-    def __init__(self, tag):
-        self.tag = tag
-
-    def __unicode__(self):
-        return self.tag
-
-class User(CacheBase):
-    """
-    Represents a Twitter user from python-twitter
-    """
-    __tablename__ = 'user'
-
-    id = sql.Column(sql.Integer, primary_key=True)
-    name = sql.Column(sql.String)
-    screen_name = sql.Column(sql.String)
-
-    def __init__(self, id, name, screen_name=None):
-        self.id = id
-        self.name = name
-        self.screen_name = screen_name or name
-
-    def __unicode__(self):
-        return self.screen_name
-
-class Status(CacheBase):
-    """
-    Represents a Status object from python-twitter
-    """
-    __tablename__ = 'status'
-
-    counter = sql.Column(sql.Integer, primary_key=True)
-    id = sql.Column(sql.Integer, unique=True)
-    user = sql.Column(sql.Integer, sql.ForeignKey('user.id'))
-    text = sql.Column(sql.String)
-    source = sql.Column(sql.String)
-    in_reply_to_status_id = sql.Column(sql.Integer)
-    in_reply_to_user_id = sql.Column(sql.Integer)
-    in_reply_to_screen_name = sql.Column(sql.String)
-    favorited = sql.Column(sql.String)
-#    created_at = sql.Column(sql.DateTime)
-
-    def __init__(self, **kwargs):
-        """
-        Initialize the tweet's information
-        """
-        for key, val in kwargs.items():
-            if hasattr(self, key):
-                if key == 'user':
-                    # handle the foreign key
-                    u = User(val['id'],
-                             val['name'],
-                             val['screen_name'])
-                    self.user = u
-                else:
-                    setattr(self, key, val)
-
-class Config(object):
-    defaults = {
-        'update_interval': (90, 'int'),
-        'last_tweet_id': (0, 'int'),
-        'login_as_user': ('', 'str'),
-        'login_as_pass': ('', 'str'),
-        'send_to_user': ('', 'str'),
-    }
-    #_session = None
-
-    def __init__(self):
-        self.engine = sql.create_engine('sqlite:///' + CONFIG, echo=False)
-
-        ConfigSession.configure(bind=self.engine)
-        self.metadata = ConfigBase.metadata
-
-        # ensure that the appropriate tables have been created
-        self.metadata.create_all(self.engine)
-        self.session = ConfigSession()
-
-        self.init_defaults()
-
-#    def _get_session(self):
-#        if not self._session or self._session.connection().closed:
-#            self._session = ConfigSession()
-#        return self._session
-#    session = property(_get_session)
-#
-#    def close(self):
-#        if self._session:
-#            self._session.close()
-
-    def _get_users(self):
-        """
-        Retrieves the information for the Twitter accounts that the user wishes
-        to check
-        """
-        results = self.session.query(TwimUser).all()
-#        self.close()
-        return results
-
-    def _set_users(self, collection):
-        """
-        Makes sure that a particular list of users is in our database
-        """
-        still_valid = [u.username for u in collection]
-        to_remove = []
-        for user in self.users:
-            if user.username not in still_valid:
-                to_remove.append(user)
-
-        for user in to_remove:
-            self.session.delete(user)
-
-        for user in collection:
-            self.session.add(user)
-        self.session.commit()
-#        self.close()
-
-    users = property(_get_users, _set_users)
-
-    def get_filtered_tags(self):
-        """
-        Retrieves any tags that should be filtered
-        """
-        results = self.session.query(FilteredTag).all()
-#        self.close()
-        return results
-
-    def add_filtered_tag(self, tag):
-        """
-        Adds a new tag to filter to the database
-        """
-        self.session.add(FilteredTag(tag))
-        self.session.commit()
-#        self.close()
-
-    def remove_filtered_tag(self, tag):
-        """
-        Removes a filtered tag from the database.  Any exceptions must be
-        handled wherever this method is called
-        """
-        obj = self.session.query(FilteredTag).filter(FilteredTag.tag==tag).one()
-        self.session.delete(obj)
-#        self.close()
-
-    def get_due_tweets(self):
-        """
-        Retrieves any scheduled tweets that are past due
-        """
-        return self.session.query(ScheduledTweet).\
-            filter(ScheduledTweet.send_at<=datetime.now()).all()
-#        self.close()
-
-    def get_scheduled_tweets(self):
-        """
-        Retrieves all scheduled tweets
-        """
-        results = self.session.query(ScheduledTweet).all()
-#        self.close()
-        return results
-
-    def add_scheduled_tweet(self, user, text, send_at, is_reminder=False):
-        """
-        Adds a scheduled tweet to the database
-        """
-        tweet = ScheduledTweet(user, text, send_at, is_reminder)
-        self.session.add(tweet)
-        self.session.commit()
-#        self.close()
-
-    def remove_scheduled_tweet(self, tweet):
-        """
-        Removes a scheduled tweet from the database
-        """
-        self.session.delete(tweet)
-#        self.close()
-
-    def init_defaults(self):
-        """
-        Initializes any default configuration settings we may have
-        """
-        for key, value in Config.defaults.items():
-            res = self.session.query(Setting).filter(Setting.key==key).first()
-            if not res:
-                default, data_type = value
-                self.session.add(Setting(key, default, data_type))
-        self.session.commit()
-#        self.close()
-
-    def __getattr__(self, key):
-        """
-        Attempts to make it easier to access configuration settings
-        """
-        if key in Config.defaults.keys():
-            res = self.session.query(Setting).filter(Setting.key==key).first()
-            default = Config.defaults[key][0]
-
-            if res: value = res.value
-            if value == None: value = default
-
-            log.debug('Getting setting "%s": %s' % (key, value))
-        else:
-            value = getattr(super(Config, self), key, None)
-#        self.close()
-
-        return value
-
-    def __setattr__(self, key, value):
-        """
-        Attempts to make it easier to save configuration settings
-        """
-#        log.debug('Attempting to set %s to %s' % (key, value))
-        if key in Config.defaults.keys():
-            res = self.session.query(Setting).filter(Setting.key==key).one()
-            setattr(res, 'value', value)
-#            self.close()
-        else:
-            super(Config, self).__setattr__(key, value)
-
-class Cache(object):
-    def __init__(self):
-        self.engine = sql.create_engine('sqlite:///' + CACHE, echo=False)
-        
-        # create a session
-        CacheSession.configure(bind=self.engine)
-        self.metadata = CacheBase.metadata
-
-        # ensure that the appropriate tables have been created (and wiped)
-        self.metadata.drop_all(self.engine)
-        self.metadata.create_all(self.engine)
-        self.session = CacheSession()
-
-    def get(self, tweet):
-        """
-        Tries to find a cached version of the tweet
-        """
-        res = self.session.query(Status).filter(Status.id==tweet.id).first()
-        if not res:
-            res = Status(**tweet.AsDict())
-            self.session.add(res)
-            self.session.commit()
-        return res
-
-    def with_id(self, id):
-        """
-        Finds a cached tweet with the specified ID or None if there's no match
-        """
-        log.info('Retrieving ID %s from cache' % id)
-        res = self.session.query(Status).filter(Status.counter==int(id)).first()
-        return res
-
-# wrap the User and Status constructors so we can cache them seamlessly
-def cache_user(func):
-    @wraps(func)
-    def wrapped(*args, **kwargs):
-        obj = func(*args, **kwargs)
-        if obj:
-            user = User(obj.id, obj.name, obj.screen_name)
-            cache.session.add(user)
-            cache.session.commit()
-        return obj
-    return wrapped
-
-def cache_status(func):
-    @wraps(func)
-    def wrapped(*args, **kwargs):
-        obj = func(*args, **kwargs)
-        if obj:
-            status = Status(**obj.AsDict())
-            cache.session.add(status)
-            cache.session.commit()
-            obj = status
-        return obj
-    return wrapped
-
-twitter.User.__init__ = cache_user(twitter.User.__init__)
-twitter.Status.__init__ = cache_status(twitter.Status.__init__)

res/twibber.icns

Binary file added.

res/twibber.ico

Added
New image

res/twibber_128x128.png

Added
New image

res/twibber_16x16.png

Added
New image

res/twibber_32x32.png

Added
New image

res/twibber_48x48.png

Added
New image

res/twim.icns

Binary file removed.

res/twim.ico

Removed
Old image

res/twim_128x128.png

Removed
Old image

res/twim_16x16.png

Removed
Old image

res/twim_32x32.png

Removed
Old image

res/twim_48x48.png

Removed
Old image

scripts/inno_setup.iss

 ; NOTE: The value of AppId uniquely identifies this application.
 ; Do not use the same AppId value in installers for other applications.
 ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
-AppId={{97B7036F-5D9E-400D-B945-59C0F6B5E2F3}
-AppName=Twim
-AppVerName=Twim 0.0.3.1
-VersionInfoVersion=0.0.3.1
+AppId={{C4008525-C8A4-4CD6-BFD7-0A9EFDD9F8A7}
+AppName=Twibber
+AppVerName=Twibber 0.0.3.5
+VersionInfoVersion=0.0.3.5
 AppPublisher=Josh VanderLinden
 AppPublisherURL=http://bitbucket.org/codekoala/twitter-im
 AppSupportURL=http://bitbucket.org/codekoala/twitter-im
 AppUpdatesURL=http://bitbucket.org/codekoala/twitter-im
-DefaultDirName={pf}\Twim
-DefaultGroupName=Twim
+DefaultDirName={pf}\Twibber
+DefaultGroupName=Twibber
 AllowNoIcons=yes
 LicenseFile=C:\dev\twitter-im\LICENSE
-OutputBaseFilename=Twim Setup
+OutputBaseFilename=Twibber Setup
 Compression=lzma
 SolidCompression=yes
 
 Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"
 
 [Files]
-Source: "C:\dev\twitter-im\dist\twim.exe"; DestDir: "{app}"; Flags: ignoreversion
+Source: "C:\dev\twitter-im\dist\twibber.exe"; DestDir: "{app}"; Flags: ignoreversion
 Source: "C:\dev\twitter-im\dist\MSVCR71.dll"; DestDir: "{app}"; Flags: ignoreversion
 Source: "C:\dev\twitter-im\dist\w9xpopen.exe"; DestDir: "{app}"; Flags: ignoreversion
 Source: "C:\dev\twitter-im\dist\library.zip"; DestDir: "{app}"; Flags: ignoreversion
 Source: "C:\dev\twitter-im\dist\README"; DestDir: "{app}"; Flags: ignoreversion
 Source: "C:\dev\twitter-im\dist\LICENSE"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\dev\twitter-im\dist\resources\*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
+Source: "C:\dev\twitter-im\dist\res\*"; DestDir: "{app}\res"; Flags: ignoreversion recursesubdirs createallsubdirs
 ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
 
 [Icons]
-Name: "{group}\Twim"; Filename: "{app}\twim.exe"; WorkingDir: "{app}"
-Name: "{group}\{cm:UninstallProgram,Twim}"; Filename: "{uninstallexe}"
-Name: "{commondesktop}\Twim"; Filename: "{app}\twim.exe"; WorkingDir: "{app}"; Tasks: desktopicon
-Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\Twim"; Filename: "{app}\twim.exe"; WorkingDir: "{app}"; Tasks: quicklaunchicon
+Name: "{group}\Twibber"; Filename: "{app}\twibber.exe"; WorkingDir: "{app}"
+Name: "{group}\{cm:UninstallProgram,Twibber}"; Filename: "{uninstallexe}"
+Name: "{commondesktop}\Twibber"; Filename: "{app}\twibber.exe"; WorkingDir: "{app}"; Tasks: desktopicon
+Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\Twibber"; Filename: "{app}\twibber.exe"; WorkingDir: "{app}"; Tasks: quicklaunchicon
 
 [Run]
-Filename: "{app}\twim.exe"; Description: "{cm:LaunchProgram,Twim}"; Flags: nowait postinstall skipifsilent
+Filename: "{app}\twibber.exe"; Description: "{cm:LaunchProgram,Twibber}"; Flags: nowait postinstall skipifsilent
 
 [UninstallDelete]
 Type: files; Name: "{app}\*.pyc"
 #!/usr/bin/env python
 """
-setup.py - script for building Twim
+setup.py - script for building Twibber
 
 Windows Usage:
     % python setup.py py2exe
 """
 
 from distutils.core import setup
+from twibber import APP_TITLE, __version__
 import sys
-import twim
-
-APP_TITLE = twim.APP_TITLE
-APP_VERSION = twim.__version__
 
 description = "A simple cross-platform Twitter client that announces Twitter updates using Jabber"
 data_files = [
         'README',
         'LICENSE'
     )),
-    ('resources', (
-        'twim.ico',
-        'twim.icns',
-        'twim_16x16.png',
-        'twim_32x32.png',
-        'twim_48x48.png',
-        'twim_128x128.png'
+    ('res', (
+        'res/twibber.ico',
+        'res/twibber.icns',
+        'res/twibber_16x16.png',
+        'res/twibber_32x32.png',
+        'res/twibber_48x48.png',
+        'res/twibber_128x128.png'
     )),
 ]
 
 if 'py2app' in sys.argv and sys.platform == 'darwin':
     import py2app
     options = dict(
-        iconfile='res/twim.icns',
+        iconfile='res/twibber.icns',
         semi_standalone=1,
         compressed=1,
         optimize=2,
         plist=dict(
             CFBundleName = APP_TITLE,
-            CFBundleShortVersionString = APP_VERSION,
+            CFBundleShortVersionString = __version__,
             CFBundleGetInfoString = '%s %s' % (APP_TITLE,
-                                               APP_VERSION),
+                                               __version__),
             CFBundleExecutable = APP_TITLE,
             CFBundleIdentifier = 'com.codekoala.%s' % APP_TITLE.lower(),
         )
     )
 
-    setup(app=['twim.py'],
+    setup(app=['twibber.py'],
           name=APP_TITLE,
-          version=APP_VERSION,
+          version=__version__,
           data_files=data_files,
           options=dict(py2app=options),
     )
             self.__dict__.update(kw)
 
             # for the versioninfo resources
-            self.version = APP_VERSION
+            self.version = __version__
             self.company_name = "Josh VanderLinden"
             self.copyright = "2009 (c) Josh VanderLinden"
             self.name = APP_TITLE
 
     RT_MANIFEST = 24
 
-    twim_target = Target(
+    twibber_target = Target(
         # used for the versioninfo resource
         description=description,
-        script="twim.py",                                     # what to build
+        script="twibber.py",                                     # what to build
         other_resources=[
             (RT_MANIFEST, 1, manifest_template % dict(prog=APP_TITLE))
         ],
-        icon_resources=[(1, 'res/twim.ico')],
-        dest_base='twim')
+        icon_resources=[(1, 'res/twibber.ico')],
+        dest_base='twibber')
 
     excludes = ['pywin', 'pywin.debugger', 'pywin.debugger.dbgcon',
                 'pywin.dialogs', 'pywin.dialogs.list']
                 ],
             }
         },
-        windows=[twim_target],
+        windows=[twibber_target],
         data_files=data_files,
     )
 else:
     setup(
         name=APP_TITLE.lower(),
-        version=APP_VERSION,
+        version=__version__,
         url='http://bitbucket.org/codekoala/twitter-im',
         author='Josh VanderLinden',
         author_email='codekoala@gmail.com',
         license='MIT',
-        scripts=['twim.py'],
+        scripts=['twibber.py'],
         data_files=data_files,
         description=description,
         keywords='twitter, python, wxpython, win, linux, mac, jabber',
             'setuptools (>=0.1)',
             'simplejson (>=0.1)',
             'xmpppy (>=0.1)',
+            'oauth (>=1.0)'
         ],
         classifiers=[
             'Development Status :: 4 - Beta',
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Twim is a simple program which regularly checks your Twitter account(s) for
+updates.  Upon finding an update, it will send it to you via Instant Message
+using any Jabber-compatible client.
+"""
+
+import logging
+import sys
+from core import Twibber
+
+APP_TITLE = 'Twibber'
+__url__ = 'http://bitbucket.org/codekoala/twitter-im'
+__version__ = '0.0.3.5'
+
+if __name__ == '__main__':
+    # configure logging
+    format = '%(levelname)s %(asctime)s %(name)s:%(funcName)s:%(lineno)d\n\t%(message)s\n'
+
+    logging.basicConfig(filename='%s.log' % APP_TITLE.lower(),
+                        filemode='w',
+                        level=logging.DEBUG,
+                        format=format)
+
+    twim = Twibber('--no-gui' not in sys.argv)

twim.py

-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Twim is a simple program which regularly checks your Twitter account(s) for
-updates.  Upon finding an update, it will send it to you via Instant Message
-using any Jabber-compatible client.
-"""
-
-import logging
-import sys
-from core import TwimApp
-
-APP_TITLE = 'Twim'
-__url__ = 'http://bitbucket.org/codekoala/twitter-im'
-__version__ = '0.0.3.5'
-
-if __name__ == '__main__':
-    # configure logging
-    format = '%(levelname)s %(asctime)s %(name)s:%(funcName)s:%(lineno)d\n\t%(message)s\n'
-
-    logging.basicConfig(filename='twim.log',
-                        filemode='w',
-                        level=logging.DEBUG,
-                        format=format)
-
-    twim = TwimApp('--no-gui' not in sys.argv)
 import urllib2
 import wx
 import xmpp
-from db import TwimUser
-from db import Config
+from data import User, config
 from wx import wizard
 from wx.lib.agw.hyperlink import HyperLinkCtrl
 
-log = logging.getLogger('twim.ui')
+log = logging.getLogger('twibber.ui')
 
 # easy way to deal with sizer flags
 sf = lambda x,y: wx.SizerFlags(wx.ALIGN_CENTER_VERTICAL).Expand().Border(x,y)
 
 class WelcomePage(wizard.WizardPageSimple):
     def __init__(self, parent):
-        from twim import APP_TITLE
+        from twibber import APP_TITLE
         wizard.WizardPageSimple.__init__(self, parent)
         self.sizer = make_page_title(self, 'Welcome to %s!' % APP_TITLE)
         info = wx.StaticText(self, label='This wizard will help you '
 
     def __init__(self, parent):
         wizard.WizardPageSimple.__init__(self, parent)
-        config = Config()
 
         # determine Twitter user information
-        for user in config.users:
+        for user in config.users.values():
             self.valid_users[user.username] = user.password
         usernames = self.valid_users.keys()
         usernames.sort()
 
 class RecipientPage(wizard.WizardPageSimple):
     def __init__(self, parent):
-        from twim import APP_TITLE
+        from twibber import APP_TITLE
         wizard.WizardPageSimple.__init__(self, parent)
-        config = Config()
 
         bold = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
         bold.SetWeight(wx.BOLD)
 
 class TransportPage(wizard.WizardPageSimple):
     def __init__(self, parent):
-        from twim import APP_TITLE
+        from twibber import APP_TITLE
         wizard.WizardPageSimple.__init__(self, parent)
-        config = Config()
 
         bold = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
         bold.SetWeight(wx.BOLD)
 class ConfigurationWizard(wizard.Wizard):
     """
     A configuration wizard that is intended to remove some of the hassles
-    involved in getting Twim up and running
+    involved in getting Twibber up and running
     """
 
-    def __init__(self, parent=None, *args, **kwargs):
-        from twim import APP_TITLE
+    def __init__(self, parent, *args, **kwargs):
+        from twibber import APP_TITLE
         title = APP_TITLE + ' Configuration Wizard'
         wizard.Wizard.__init__(self, parent, title=title)
-        config = Config()
+        self.parent = parent
 
         # steps
         # - configure twitter account(s)
         Persist all of the configuration changes to disk
         """
         log.info('Persisting configuration to disk')
-        config = Config()
 
         # save Twitter users
-        existing = dict((u.username, u.password) for u in config.users)
-        current_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:
+                for user in config.users.values():
                     if user.username == username:
                         user.password = password
-                        current_users.append(user)
             else:
                 log.info('adding %s to configuration' % username)
-                current_users.append(TwimUser(username, password))
+                user = User(username, password)
+            current_users[username] = user
         config.users = current_users
 
-        # Persist() is automatically called for each of these
         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.session.commit()
+        config.persist()
+
+        # make sure we enable or disable the "as" command if the user changed
+        # the number of Twitter users
+        self.parent.twibber.InitCommands()
 
 class TwitterTray(wx.TaskBarIcon):
     """
     A simple tray icon to tell people that the program is running
     """
     def __init__(self, parent):
-        from twim import APP_TITLE, __version__
+        from twibber import APP_TITLE, __version__
         wx.TaskBarIcon.__init__(self)
 
         if sys.platform != 'darwin':
             # The .icns looks better on OSX than this embedded image
-            icon = wx.Icon('res/twim_32x32.png', wx.BITMAP_TYPE_PNG)
+            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)
             self.SetIcon(icon, '%s v%s' % (APP_TITLE, __version__))
 
         self.menu = None
 
     def GetMenu(self):
         if not self.menu:
-            from twim import APP_TITLE
+            from twibber import APP_TITLE
             self.menu = wx.Menu()
             self.menu.Append(wx.ID_SETUP, 'Configure %s' % APP_TITLE)
             self.menu.AppendSeparator()
     def OnShowMenu(self, evt):
         self.PopupMenu(self.GetMenu())
 
-class TwimGUI(wx.Frame):
-    def __init__(self, twim, app):
-        self.twim = twim
+class TwibberGUI(wx.Frame):
+    def __init__(self, twibber, app):
+        self.twibber = twibber
         self.app = app
 
         wx.Frame.__init__(self, parent=None)
-        config = Config()
 
         # check for some configuration
         requires_config = False
         self.tray.Bind(wx.EVT_CLOSE, self.OnExit)
         self.tray.Bind(wx.EVT_MENU, self.OnMenu)
 
-        self.twim.BeginBeingUseful()
+        self.twibber.BeginBeingUseful()
 
     def OnConfiguration(self, evt=None):
         """
         ConfigurationWizard(self)
 
         # force a reconnect to Jabber
-        self.twim._client = None
+        self.twibber._client = None
 
     def OnExit(self, evt=None):
         """
             # don't bother with dead objects :D
             pass
         
-        self.twim.OnExit()
+        self.twibber.OnExit()
 
     def OnMenu(self, evt):
         """
         if action:
             action(evt)
 
-def main(twim):
-    app = wx.App()
-    TwimGUI(twim, app)
+def main(twibber):
+    app = wx.App(redirect=True, filename='wx.log')
+    TwibberGUI(twibber, app)
     app.MainLoop()