#!/usr/bin/env python

"""
Twixer - Advanced command line Twitter client (maybe not yet that advanced!)
(C) 2008-2010 - Antonio Ognio <gnrfan@gmail.com>
License: Artistic License / GPL v2 (your choice)

Instalation:

  - Install simple-json python module: http://pypi.python.org/pypi/simplejson/
  - Install tweethon python module: http://bitbucket.org/jrossi/tweethon
  - Copy this file to /usr/bin/twixer or wherever you want to (you must know what you're doing)
  - Give twixer proper execute permissions: eg. sudo chmod +x /usr/bin/twixer
  - Create a hidden file in your $HOME called .twixerc with just this three lines:
  
    [account]
    username = <your username>
    password = <your password>
    server = <base url for API server>

    Examples of server configurations:

    server = http://twitter.com

    or

    server = http://identi.ca/api
    
  - You're ready to go!

IMPORTANT:

For Twitter, please run twixer -o or --oauth-config and add the returned credentials to your $HOME/.twixerrc config file.

Usage:

  twixer --help

"""

import sys
import os
import ConfigParser
import optparse
import urllib2
import tweethon
import oauth2 as oauth
from oauthtwitter import OAuthApi

class TwixerNoChanges(Exception):
    pass

class TwixerDataError(Exception): 
    pass
    
class TwixerAccountError(Exception): 
    pass

class TwixerApp:
    
    APPNAME = 'twixer'
    VERSION = '0.1.99'
    WEBSITE = 'http://code.google.com/p/twixer/'
    ENCODING = 'utf8'

    DEFAULT_SERVER = 'http://twitter.com'

    def __init__(self):
        self.initializeOptionParser()
        self.initializeConfigParser()
        self.parseOptions()
        self.readConfiguration()
        self.credentialsFromConfig()
        self.initializeApi()
        
    def initializeOptionParser(self):
        self.op = optparse.OptionParser(version='%s %s' % (self.APPNAME, self.VERSION)) 
        self.op.add_option('-c', '--config', action='store', type='string', dest='confpath', default=None, help='Path to configuration file')
        self.op.add_option('-v', '--verbose',  dest='verbose',  default=False)
        self.op.add_option('-U', '--username', action='store', type='string', dest='username', default=None, help='Username for your Twitter account', metavar='USERNAME')
        self.op.add_option('-P', '--password', action='store', type='string', dest='password', default=None, help='Password for your Twitter account', metavar='PASSWORD')
        self.op.add_option('-S', '--server', action='store', type='string', dest='server', default=None, help='API server for microblogging service', metavar='SERVER')
        self.op.add_option('-u', '--query-user', action='store', type='string', dest='query_user', default=None, help='Query USERNAME instead of your user', metavar='USERNAME')
        self.op.add_option('-t', '--timeline', action='store_true', dest='timeline', default=None, help='Show timeline of user')
        self.op.add_option('-d', '--direct', action='store', type='string', dest='direct',  default=None, help='Send direct message to user', metavar='USERNAME')
        self.op.add_option('-f', '--friends',  action='store_true', dest='friends', default=False, help='Show friends timeline')
        self.op.add_option('-r', '--replies',  action='store_true', dest='replies', default=False, help='Show replies')
        self.op.add_option('-R', '--direct-replies',  action='store_true', dest='direct_replies', default=False, help='Show direct messages or replies')
        self.op.add_option('-p', '--public', action='store_true', dest='public', default=False, help='Show public timeline')
        self.op.add_option('-a', '--follow', action='store', type='string', dest='follow', default=None, help='Follow user', metavar='USERNAME')
        self.op.add_option('-s', '--stop-following', action='store', type='string', dest='stop_following', default=None, help='Stop following user', metavar='USERNAME')
        self.op.add_option('-D', '--delete-status', action='store', type='int', dest='delete_status', default=None, help='Delete status by id', metavar='ID')
        self.op.add_option('-X', '--delete-direct-message', action='store', type='int', dest='delete_direct_message', default=None, help='Delete direct message by id', metavar='ID')
        self.op.add_option('-l', '--list-friends', action='store_true', dest='list_friends', default=None, help='Show list of friends')
        self.op.add_option('-F', '--list-followers', action='store_true', dest='list_followers', default=None, help='Show list of followers')
        self.op.add_option('-i', '--info', action='store_true', dest='info', default=None, help='Show information on user')
        self.op.add_option('-o', '--oauth-config', action='store_true', dest='oauth', default=None, help='Request OAuth token and secret')
        
    def initializeConfigParser(self):
        self.cp = ConfigParser.SafeConfigParser()
        self.cp.add_section('account')
        self.cp.add_section('oauth')
        self.cp.add_section('preferences')

    def isOAuth(self):
        return getattr(self.options, 'username', False) and \
               getattr(self.options, 'oauth_token', False) and \
               getattr(self.options, 'oauth_token_secret', False) and \
               getattr(self.options, 'oauth_consumer_key', False) and \
               getattr(self.options, 'oauth_consumer_secret', False)

    def isPasswordAuth(self):
        return getattr(self.options, 'username', False) and \
               getattr(self.options, 'password', False) and \
               not self.isOAuth()
    
    def initializeApi(self):
        if self.isOAuth():
            self.api = OAuthApi(
                self.options.oauth_consumer_key, 
                self.options.oauth_consumer_secret, 
                self.options.oauth_token, 
                self.options.oauth_token_secret)
            self.authenticated = True
        elif self.isPasswordAuth():
            self.api = tweethon.Api(
                            username=self.options.username, 
                            password=self.options.password,
                            base_url=self.options.server)
            self.authenticated = True
            self.api.SetSource(self.APPNAME)
        else:
            self.api = tweethon.Api()
            self.authenticated = False

    
    def getDefaultConfigPath(self):
        return os.path.expanduser(os.path.join('~', '.twixerrc'))
    
    def getConfigPath(self):
        confpath = None
        try:
            confpath = self.options.confpath
        except AttributeError:
            pass
        if confpath is None:
            return self.getDefaultConfigPath()
    
    def readConfiguration(self):
        self.cp.read(self.getConfigPath())

    def parseOptions(self):
        self.options, self.args = self.op.parse_args()
        self.msg = ' '.join(self.args).encode(self.ENCODING)

    def credentialsFromConfig(self):
        try:
            if self.options.username is None:
                self.options.username = self.cp.get('account', 'username')

            if self.options.password is None:
                self.options.password = self.cp.get('account', 'password')

            if self.options.server is None:
                self.options.server = self.cp.get('account', 'server')
            
            try:
                self.options.oauth_token = self.cp.get('oauth', 'token')
            except Exception, e:
                self.options.oauth_token = None

            try:
                self.options.oauth_token_secret = self.cp.get('oauth', 'token_secret', None)
            except Exception, e:
                self.options.oauth_token_secret = None

            try:
                self.options.oauth_consumer_key = self.cp.get('oauth', 'consumer_key', None) 
            except Exception, e:
                self.options.oauth_consumer_key = None
            
            try:
                self.options.oauth_consumer_secret = self.cp.get('oauth', 'consumer_secret', None) 
            except Exception, e:
                self.options.oauth_consumer_secret = None
            
        except ConfigParser.NoOptionError, e:
            print e
        
        if self.options.username is None or self.options.password is None:
            raise TwixerAccountError, 'Account Error'

        if self.options.server is None:
            self.options.server = self.DEFAULT_SERVER

    def getQueryUsername(self):
        if self.options.query_user:
            return self.options.query_user
        else:
            return self.options.username

    def listFriends(self):
        username = self.getQueryUsername()
        print "Retrieving %s's list of friends..." % (username),
        try:
            if self.isOAuth():
                options = {}
                options['screen_name'] = username
                friends = self.api.GetFriends(options)
            else:
                friends = self.api.GetFriends(username)
            print "done."
            print "%s is following %d friend(s):" % (username, len(friends))
            for f in friends:
                screen_name = f.screen_name.encode(self.ENCODING)
                name = f.name.encode(self.ENCODING)
                if screen_name == name:
                    print screen_name
                else:
                    print "%s (%s)" % (screen_name, name)
        except urllib2.HTTPError, e:
            print "failed."
            self.processHTTPError(e)

    def listFollowers(self):

        if self.options.query_user:
            sys.stderr.write("ERROR: You can only list the followers of the currently authenticated user.\n")
            return

        username = self.options.username
        print "Retrieving %s's list of followers..." % (username),
        try:
            followers = self.api.GetFollowers()
            print "done."
            print "%s has %d follower(s):" % (username, len(followers))
            for f in followers:
                screen_name = f.screen_name.encode(self.ENCODING)
                name = f.name.encode(self.ENCODING)
                if screen_name == name:
                    print screen_name
                else:
                    print "%s (%s)" % (screen_name, name)
        except urllib2.HTTPError, e:
            print "failed."
            self.processHTTPError(e)        

    def userInfo(self):
        username = self.getQueryUsername()
        print "Retriving information on user %s..." % (username),
        try:
            user_info = self.api.GetUser(username)
            print "done."
            print "User Id: %d" % (user_info.id)    
            print "Screen name: %s" % (user_info.screen_name.encode(self.ENCODING))    
            print "Full name: %s" % (user_info.name.encode(self.ENCODING))    
            print "Description:"
            print user_info.name.encode(self.ENCODING)
            print "URL: %s" % (user_info.url.encode(self.ENCODING))    
            print "Lastest status:"
            print "%s (%s)" % (user_info.status.text.encode(self.ENCODING), \
                          user_info.status.created_at.encode(self.ENCODING))
        except urllib2.HTTPError, e:
            print "failed."
            self.processHTTPError(e)


    def printStatus(self, status):
        print "Status %s by @%s at %s:" % (status.id, \
                                                      status.user.screen_name.encode(self.ENCODING), \
                                                      status.created_at.encode(self.ENCODING))
        print status.text.encode(self.ENCODING), "\n"

    def printDirectMessage(self, direct_message):
        print "Direct message %d from %s for %s sent at %s:" % (direct_message.id, \
                                                                        direct_message.sender_screen_name.encode(self.ENCODING), \
                                                                        direct_message.recipient_screen_name.encode(self.ENCODING), \
                                                                        direct_message.created_at.encode(self.ENCODING))
        print direct_message.text.encode(self.ENCODING), "\n"

    def printTimeline(self, timeline, title):
        print "%s - Latest %d item(s):" % (title, len(timeline)), "\n"
        for status in timeline:
            self.printStatus(status)

    def publicTimeline(self):
        print "Retrieving status in public timeline...",
        try:
            timeline = self.api.GetPublicTimeline()
            print "done."
            self.printTimeline(timeline, 'Public Timeline')
        except urllib2.HTTPError, e:
            print "failed."
            self.processHTTPError(e)

    def friendsTimeline(self):
        username = self.getQueryUsername()
        print "Retrieving status in %s's friends timeline..." % (username),
        try:
            timeline = self.api.GetFriendsTimeline()
            print "done."
            self.printTimeline(timeline, "%s's friends timeline" % (username))
        except urllib2.HTTPError, e:
            print "failed."
            self.processHTTPError(e)

    def userTimeline(self):
        username = self.getQueryUsername()
        print "Retrieving status in %s's timeline..." % (username),
        try:
            if self.isOAuth():
                options = {}
                options['screen_name'] = username
                timeline = self.api.GetUserTimeline(options)
            else:
                timeline = self.api.GetUserTimeline(username)
            print "done."
            self.printTimeline(timeline, "%s's timeline" % (username))
        except urllib2.HTTPError, e:
            print "failed."
            custom_messages = {
                401 : "Updates from user %s are protected and only available for friends. Try to follow the user first." % (username)
            }
            self.processHTTPError(e, custom_messages)

    def userReplies(self):
        
        if self.options.query_user:
            sys.stderr.write("ERROR: You can only list the replies to the currently authenticated user.\n")
            return

        print "Retrieving replies for %s..." % (self.options.username)
        try:
            replies = self.api.GetReplies()
            print "done."
            self.printTimeline(replies, "%s's replies" % (self.options.username))
        except urllib2.HTTPError, e:
            print "failed."
            self.processHTTPError(e)

    def checkUserIsFriend(self, username, friend):
        try:
            if self.isOAuth():
                options = {}
                options['screen_name'] = username
                friends = self.api.GetFriends(options)
            else:
                friends = self.api.GetFriends(username)
            for f in friends:
                if f.screen_name == friend:
                    return True
            return False
        except urllib2.HTTPError, e:
            self.processHTTPError(e)

    def followUser(self, username):
        if self.checkUserIsFriend(self.options.username, username):
            sys.stderr.write("ERROR: You're already following %s\n" % (username))
        else:
            try:
                if self.isOAuth():
                    friend = self.api.FollowUserByScreenname(username)
                else:
                    friend = self.api.CreateFriendship(username)
                if friend != None:
                    print "You are following %s now." % (username)
            except urllib2.HTTPError, e:
                self.processHTTPError(e)

    def stopFollowingUser(self, username):
        if self.checkUserIsFriend(self.options.username, username) == False:
            sys.stderr.write("ERROR: You're not following %s\n" % (username))
        else:
            try:
                friend = self.api.DestroyFriendship(username)
                if friend != None:
                    print "You are no longer following %s." % (username)
            except urllib2.HTTPError, e:
                self.processHTTPError(e)
                

    def checkMessageLength(self, msg):
        return len(msg)>0 and len(msg)<=140

    def postUpdate(self):
        #FIXME: Implement all sort of checks and fixes before sending message
        if self.checkMessageLength(self.msg):
            try:
                if self.isOAuth():
                    status = self.api.UpdateStatus(self.msg)
                else :
                    status = self.api.PostUpdate(self.msg)
                print "Status %d successfully posted at %s" % (status.id, status.created_at.encode(self.ENCODING))
            except urllib2.HTTPError, e:
                self.processHTTPError(e)
        else:
            sys.stderr.write("ERROR: The message must be 140 or less characters long. The current length is %d characters.\n" % len(self.msg))

    def deleteStatus(self, id):
        try:
            status = self.api.DestroyStatus(id)
            print "Status %d posted by %s at %s has been deleted successfully." % (status.id, \
                                                                                       status.user.screen_name.encode(self.ENCODING), \
                                                                                       status.created_at.encode(self.ENCODING))
        except urllib2.HTTPError, e:
            self.processHTTPError(e)            

    def sendDirectMessage(self):
        #FIXME: Implement all sort of checks and fixes before sending message
        if self.checkMessageLength(self.msg):
            try:
                direct_message = self.api.PostDirectMessage(self.options.direct, self.msg)
                print "Direct message %d successfully sent to %s at %s" % (direct_message.id, \
                                                                                           direct_message.recipient_screen_name.encode(self.ENCODING), \
                                                                                           direct_message.created_at.encode(self.ENCODING))
            except urllib2.HTTPError, e:
                custom_messages = {
                    403: "For some reason you can't send this direct message."
                }
                self.processHTTPError(e, custom_messages)
        else:
            sys.stderr.write("ERROR: Direct messages must be 140 or less characters long. The current length is %d characters.\n" % len(self.msg))
    
    def directReplies(self):
        
        if self.options.query_user:
            sys.stderr.write("ERROR: You can only list the direct message replies to the currently authenticated user.\n")
            return

        print "Retrieving direct messages for %s..." % (self.options.username),
        try:
            replies = self.api.GetDirectMessages()
        except urllib2.HTTPError, e:
            self.processHTTPError(e)

        print "done."
        print "Direct messages for %s:" % (self.options.username)
        for r in replies:
            self.printDirectMessage(r)

    def deleteDirectMessage(self, direct_message_id):
        try:
            direct_message = self.api.DestroyDirectMessage(direct_message_id)
        except urllib2.HTTPError, e:
            self.processHTTPError(e)            

        print "Direct message %d sent by %s to %s at %s has been deleted successfully." % (direct_message.id, \
                                                                                           direct_message.sender_screen_name.encode(self.ENCODING), \
                                                                                                   direct_message.recipient_screen_name.encode(self.ENCODING), \
                                                                                           direct_message.created_at.encode(self.ENCODING))

    def processHTTPError(self, e, custom_messages={}):
        if e.code == 400:
            if custom_messages.has_key(400):
                error_msg = custom_messages[400]
            else:
                error_msg  = "Twitter returned a \"Bad Request\" error. "
                error_msg += "This happens when you exceed 70 requests pero hour. "
                error_msg += "Avoid running other Twitter applications or refreshing too frequently."

        if e.code == 401:
            if custom_messages.has_key(401):
                error_msg = custom_messages[401]
            else:
                error_msg = "Twitter returned an \"Unauthorized\" error. "
                error_msg += "This happens when you're not authenticated, your credentials aren't valid "
                error_msg += "or you don't have the required authorization level to perform the requested operation."

        elif e.code == 403:
            if custom_messages.has_key(403):
                error_msg = custom_messages[403]
            else:
                error_msg  = "Twitter returned a \"Forbidden\" error. "
                error_msg += "This means you don't have permission to perform the requested operation."

        elif e.code == 404:
            if custom_messages.has_key(404):
                error_msg = custom_messages[404]
            else:
                error_msg  = "Twitter returned an \"Object Not Found\" error. "
                error_msg += "This means the resource you're trying to access does not exist."

        elif e.code == 500:
            if custom_message.has_key(500):
                error_msg = custom_messages[500]
            else:
                error_msg  = "Twitter returned an \"Internal Server Error\" error. "
                error_msg  = "This means the servers at Twitter are having some problems. "
                error_msg += "Please try again this operation in a few minutes."        
                error_msg += "If the error persists consider reporting the problem to the Twitter team."

        elif e.code == 502:
            if custom_message.has_key(502):
                error_msg = custom_messages[502]
            else:
                error_msg  = "Twitter returned a \"Bad Gateway\" error. "
                error_msg += "This normally means the servers at Twitter are down "
                error_msg += "or under mantainance."        
    
        elif e.code == 503:
            if custom_message.has_key(503):
                error_msg = custom_messages[503]
            else:
                error_msg  = "Twitter returned a \"Service Unavailable\" error. "
                error_msg += "This normally means the servers at Twitter are up "
                error_msg += "but overloaded with request. "        
                error_msg += "Please retry this operation later."        
        else:
            error_msg = e.message

        self.printErrorMessage(error_msg)
        sys.exit(1)

    def oAuthConfig(self):
        consumer_key = "br50V6q8HAP0NyFZ7ueiJw"
        consumer_secret = "8S9tEtPXg4awaXFwzhJinOASu5Y5vdLNvT86YeDbKEY"
        twitter = OAuthApi(consumer_key, consumer_secret)

        # Get the temporary credentials for our next few calls
        temp_credentials = twitter.getRequestToken()

        # User pastes this into their browser to bring back a pin number
        print("Please, copy & paste this URL to your web browser:\n")
        print(twitter.getAuthorizationURL(temp_credentials))
        print("\nYou'll have to authorize the 'twixercli' app and then copy and paste the given PIN here.\n")

        # Get the pin # from the user and get our permanent credentials
        oauth_verifier = raw_input('What is the PIN? ')
        access_token = twitter.getAccessToken(temp_credentials, oauth_verifier)

        print("Please add the following to your $HOME/.twixerrc configuration file:\n");
        print("[oauth]")
        print("#Twitter OAuth consumer credential for Twixer.\n")
        print("consumer_key = %s" % consumer_key)
        print("consumer_secret = %s\n" % consumer_secret)
        print("token = %s" % access_token['oauth_token'])
        print("token_secret = %s" % access_token['oauth_token_secret'])

    def printErrorMessage(self, error_msg):
        sys.stderr.write("ERROR: " + error_msg + "\n")

    def run(self):
        if self.options.list_friends:
            self.listFriends()
        elif self.options.public:
            self.publicTimeline()
        elif self.options.friends:
            self.friendsTimeline()
        elif self.options.timeline:
            self.userTimeline()
        elif self.options.replies:
            self.userReplies()
        elif self.options.list_followers:
            self.listFollowers()
        elif self.options.follow:
            self.followUser(self.options.follow)
        elif self.options.stop_following:
            self.stopFollowingUser(self.options.stop_following)
        elif self.options.delete_status:
            self.deleteStatus(self.options.delete_status)
        elif self.options.direct:
            self.sendDirectMessage()
        elif self.options.direct_replies:
            self.directReplies()
        elif self.options.delete_direct_message:
            self.deleteDirectMessage(self.options.delete_direct_message)
        elif self.options.info:
            self.userInfo()
        elif self.options.oauth:
            self.oAuthConfig()
        elif len(self.msg)>0:
            self.postUpdate()
        else:
            self.op.print_help()

if __name__ == "__main__":
    Twixer = TwixerApp()
    Twixer.run()
