#!/usr/bin/env python
#

"""
gtweet: Send or receive tweets, and display them

To runs as a widget:

  gtweet -w -f -s topic > $GTERM_SOCKET &
"""

import binascii
import cgi
import cStringIO as StringIO
import csv
import json
import logging
import os
import random
import rfc822
import sys
import time
import urllib
import uuid

import tornado.auth
import tornado.httpclient

try:
    from urllib.request import urlopen
    from urllib.parse import urlencode
except ImportError:
    from urllib import urlopen, urlencode

import gterm

CONFIG_HELP = """Steps to acquire consumer and access tokens:
  1. Register an application with twitter at dev.twitter.com
  2. Find authentication info at https://dev.twitter.com/docs/auth/tokens-devtwittercom
  3. Save authentication tokens in ~/.twitter_auth using the following JSON format (with consumer key/secret corresponding to the API key/secret):
"""

AUTH_FORMAT = """
      {"consumer_token":  {"consumer_key": "%(consumer_key)s",
                           "consumer_secret": "%(consumer_secret)s"},
       "access_token": {"key": "%(access_key)s",
                        "secret": "%(access_secret)s"}
      }
"""

AUTH_HELP = AUTH_FORMAT % {"consumer_key": "...",
                           "consumer_secret": "...",
                           "access_key": "12345678-...",
                           "access_secret": "..."}

# Note: Having a finite timeout causes problems with tornado's simple httpclient
TWITTER_STREAM_TIMEOUT_SEC = 0

TWITTER_STREAM_MINDELAY = 30        # Min. delay time for reconnecting to twitter stream
TWITTER_STREAM_MAXDELAY = 3600      # Max. delay time for reconnecting to twitter stream

TWITTER_URL_PREFIX = "https://api.twitter.com/1"
TWITTER_STATUS_PATH = "/statuses/update"
TWITTER_STREAM_URL = "https://stream.twitter.com/1/statuses"
TWITTER_USER_STREAM_URL = "https://userstream.twitter.com/2"
##TWITTER_USER_STREAM_URL = "http://localhost:4079"

Auth_parser = gterm.FormParser(title='Enter OAuth credentials for your Twitter app<br>(Usually at <a href="https://dev.twitter.com/apps" target="_blank">https://dev.twitter.com/apps</a>)<p>')

Auth_parser.add_option("consumer_key", label="Consumer key: ", help="Consumer key")
Auth_parser.add_option("consumer_secret", label="Consumer secret: ", help="Consumer secret")
Auth_parser.add_option("access_key", label="Access token: ", help="Access token")
Auth_parser.add_option("access_secret", label="Access token secret: ", help="Access token secret")

pagelet_html = """<script>
var GTPageletMaxTweets = 36;
function GTPageletJSON(pageletElem, tweet) {
    // Display new tweet, sliding it into the top line
    $('<li><img src="/_static/images/twitter_bird_32.png"> '+tweet+'<p></li>').hide().prependTo(pageletElem.find("ul")).slideDown("slow").animate({opacity: 1.0});
    if (pageletElem.find("ul li").length > GTPageletMaxTweets)
        pageletElem.find("ul li:last-child").remove();
}

</script>
<style>
.gterm-pagelet-gtweets-container ul {
   list-style: none;
}
</style>
<div class="gterm-pagelet-gtweets-container">
<ul>
</ul>
</div>
"""

DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
def parsedate(rfc822_time):
    """Return (epoch_time, YYYY-mm-ddThh:mm:ss (local))"""
    time_list = list(rfc822.parsedate_tz(rfc822_time))
    time_list[9] = 0 # Zero out time zone information (for UTC)
    epoch_time = rfc822.mktime_tz(tuple(time_list))
    time_str = time.strftime(DATE_FORMAT, time.localtime(epoch_time))
    return (epoch_time, time_str)


def twitter_request(consumer_token, path, path_prefix=TWITTER_URL_PREFIX, access_token=None,
                    get_args={}, post_args=None, streaming_callback=None,
                    connect_timeout=20.0, timeout=1200.0):
    url = path_prefix + path + ".json"
    method = "POST" if post_args is not None else "GET"
    query_args = get_args.copy()
    if access_token:
        all_args = get_args.copy()
        all_args.update(post_args or {})
        consumer_token = dict(key=consumer_token["consumer_key"], secret=consumer_token["consumer_secret"])
        oauth = oauth_request_parameters(consumer_token, url, access_token, all_args, method=method)
        query_args.update(oauth)

    if query_args: url += "?" + urllib.urlencode(query_args)
    post_data = urllib.urlencode(post_args) if post_args is not None else None
    return tornado.httpclient.HTTPRequest(str(url), method,
                                          headers={"Connection": "keep-alive"},
                                          body=post_data,
                                          connect_timeout=connect_timeout,
                                          request_timeout=timeout,
                                          streaming_callback=streaming_callback)

def oauth_request_parameters(consumer_token, url, access_token, parameters={},
                             method="GET", oauth_version="1.0a",
                             override_version=""):
    base_args = dict(
        oauth_consumer_key=consumer_token["key"],
        oauth_token=access_token["key"],
        oauth_signature_method="HMAC-SHA1",
        oauth_timestamp=str(int(time.time())),
        oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
        oauth_version=override_version or oauth_version,
    )
    args = base_args.copy()
    args.update(parameters)
    if oauth_version == "1.0a":
        signature = tornado.auth._oauth10a_signature(consumer_token, method, url, args, access_token)
    else:
        signature = tornado.auth._oauth_signature(consumer_token, method, url, args, access_token)
    base_args["oauth_signature"] = signature
    return base_args

def send_request(path, path_prefix=TWITTER_URL_PREFIX, get_args={}, post_args=None, streaming_callback=None):
    http_request = twitter_request(Auth_tokens["consumer_token"],
                                   path, path_prefix=path_prefix,
                                   access_token=Auth_tokens["access_token"],
                                   get_args=get_args,
                                   post_args=post_args,
                                   streaming_callback=streaming_callback,
                                   connect_timeout=60.0,
                                   timeout=float(options.timeout))

    http_client = tornado.httpclient.HTTPClient()
    try:
        return http_client.fetch(http_request)
    except tornado.httpclient.HTTPError, excp:
        return excp

        
def update_status(text):
    post_args = {"status": text}
    resp = send_request(TWITTER_STATUS_PATH, post_args=post_args)
    if isinstance(resp, Exception):
        print >> sys.stderr, resp
            
Twitter_stream_restart_time = time.time()
Twitter_stream_delay = 0
Twitter_user_stream = None

class TwitterStreamReader(object):
    def __init__(self, my_id, friend_set=set()):
        self.user_id = my_id
        self.friend_set = friend_set
        self.buffer = ""
        logging.debug("id=%s, friend_set=%s", my_id, friend_set)

    def response_callback(self, response):
        global Twitter_stream_restart_time, Twitter_stream_delay, Twitter_user_stream
        if isinstance(response, basestring):
            logging.info("StreamReadResponse: %s", response)
        else:
            logging.error("StreamReadError: %s", response)

        Twitter_user_stream = None
        if not Twitter_stream_delay:
            Twitter_stream_delay = TWITTER_STREAM_MINDELAY
        else:
            Twitter_stream_delay = min(2*Twitter_stream_delay, TWITTER_STREAM_MAXDELAY)
        Twitter_stream_restart_time = time.time() + Twitter_stream_delay

    def handle_stream(self, data):
        global Twitter_stream_restart_time, Twitter_stream_delay, Twitter_user_stream
        self.buffer += data
        if "\r\n" not in self.buffer:
            # Wait for complete line
            return
        line, sep, self.buffer = self.buffer.partition("\r\n")
        if not line:
            return

        try:
            content = json.loads(line)
        except Exception, excp:
            print >> sys.stderr, "JSON decoding error '%s': %s" % (excp, line)
            sys.exit(1)
        self.buffer = ""

        logging.debug(":twitter_stream: content=%s", str(content))

        user_info = None

        message = {}
        if "direct_message" in content:
            dmesg = content["direct_message"]
            message["type"] = "direct"
            created_at = dmesg["created_at"]
            text = dmesg.get("text", "")
            message["user"] = dmesg["sender"]
            message["id"] = dmesg.get("id", 0)

        elif "text" in content:
            to_user_id = content.get("in_reply_to_user_id")
            if to_user_id and to_user_id != self.user_id and to_user_id not in self.friend_set:
                logging.info(":twitter_stream: dropped message to unknown dest", to_user_id, self.user_id)
                return
            created_at = content["created_at"]
            text = content.get("text", "")
            message["type"] = "tweet"
            message["user"] = content["user"]
            message["id"] = content.get("id", 0)

        else:
            # Unknown content
            return

        message["time"] = parsedate(created_at)[1]
        message["text"] = text.encode("ascii", "ignore")

        user_info = message["user"]
        user_info["name"] = user_info["name"].encode("ascii", "ignore")

        user_label = "%(screen_name)s" % user_info

        logging.debug(":msg: %s[id=%s]: %s", user_label, message["id"], message["text"])

        if options.csv or options.json or options.text:
            if options.csv:
                fields = [message["time"], message["type"], user_info["screen_name"], user_info["id"], user_info["name"], message["text"]]
                csvfile = StringIO.StringIO()
                csvwriter = csv.writer(csvfile)
                csvwriter.writerow(fields)
                msg_out = csvfile.getvalue()
            elif options.json:
                msg_out = json.dumps(message)
            else:
                msg_out = "%s: %s\n" % (user_label, message["text"])
            if options.fullscreen:
                if not sys.stderr.isatty():
                    print >> sys.stderr, msg_out
                    sys.stderr.flush()
            else:
                print >> sys.stdout, msg_out
                sys.stdout.flush()

        if options.fullscreen:
            if options.anon:
                tweet_html = cgi.escape(message["text"])
            else:
                tweet_html = "<b>%s:</b> %s" % (cgi.escape(user_label), cgi.escape(message["text"]))
            gterm.wrap_write(json.dumps(tweet_html), headers=Json_headers, stderr=(not Out_tty))

def read_twitter_user_stream(Auth_tokens, keywords=[], recv=False):
    global Twitter_stream_restart_time, Twitter_stream_delay, Twitter_user_stream

    my_id = long( Auth_tokens["access_token"]["key"].partition("-")[0] )
    if Twitter_user_stream:
        return my_id

    logging.info("*********TWITTER STREAM*****my_id=%s", my_id)
    Twitter_user_stream = TwitterStreamReader(my_id)

    if recv:
        get_args = {"track": ",".join(keywords)} if keywords else {}
        post_args = None
        path = "/user"
        stream_url = TWITTER_USER_STREAM_URL
    else:
        # Search
        if not keywords:
            raise Exception("No keywords to search for!")
        get_args = {}
        post_args = {"track": ",".join(keywords)}
        path = "/filter"
        stream_url = TWITTER_STREAM_URL

    resp = send_request(path, path_prefix=stream_url, get_args=get_args, post_args=post_args,
                        streaming_callback=Twitter_user_stream.handle_stream)
    if isinstance(resp, Exception):
        print >> sys.stderr, resp

    return my_id

def twitter_stream_watcher():
    """ Checks if Twitter stream feed is active, and restarts it if it is not.
    """
    global Twitter_stream_restart_time, Twitter_stream_delay, Twitter_user_stream
    if Twitter_stream_restart_time and (time.time() >= Twitter_stream_restart_time):
        logging.info("************ TWITTER STREAM READER STARTING (%d) *************" % Twitter_stream_delay)
        Twitter_stream_restart_time = 0
        read_twitter_user_stream()

def check_auth_file(auth_file):
    auth_file = os.path.expanduser(auth_file)
    if not os.path.isfile(auth_file):
        if not Out_tty:
            print >> sys.stderr, "Authentication file %s not found!\n\n%s%s" % (auth_file, CONFIG_HELP, AUTH_HELP)
            sys.exit(1)
        try:
            form_values = Auth_parser.read_input(trim=True)
            if not form_values:
                raise Exception("Form input cancelled")

            if not all(form_values.values()):
                raise Exception("Missing authentication data")
                
            with os.fdopen( os.open(auth_file, os.O_WRONLY|os.O_CREAT, 0600), "w") as f:
                f.write(AUTH_FORMAT % form_values)
        except Exception, excp:
            print >> sys.stderr, "Error in creating authentication file: %s" % excp
            sys.exit(1)

    with open(auth_file) as f:
        auth_json = f.read()
    try:
        global Auth_tokens
        Auth_tokens = json.loads(auth_json)
    except Exception, excp:
        print >> sys.stderr, "Syntax error in authentication file %s: %s\n\nExpecting:\n%s" % (auth_file, excp, AUTH_HELP)
        sys.exit(1)

Default_auth_file = "~/.twitter_auth"

Out_tty = False
if len(sys.argv) < 2:
    check_auth_file(Default_auth_file)

usage = "usage: %prog [-h ...] [keyword1] ..."
form_parser = gterm.FormParser(usage=usage, title="Text to tweet or keywords to search for: ", command="gtweet")

form_parser.add_argument(help="Tweet text for sending, or keywords for receiving")
form_parser.add_option("search", False, short="s", help="Search for all tweets containing specified keywords")
form_parser.add_option("recv", False, short="r", help="Receive tweets mentioning this user or any keywords")
form_parser.add_option("anon", False, short="a", help="Anonymize (hide user names in public display)")

form_parser.add_option("fullscreen", False, short="f", help="Fullscreen display", raw=True)
form_parser.add_option("widget", False, short="w", help="Widget display", raw=True)
form_parser.add_option("csv", False, short="c", help="CSV output", raw=True)
form_parser.add_option("json", False, short="j", help="JSON output", raw=True)
form_parser.add_option("text", False, short="t", help="Text output", raw=True)
form_parser.add_option("opacity", 1.0, short="o", help="Feed opacity (default: 1.0)")
form_parser.add_option("timeout", 0.0, help="Timeout in sec (default: 0 => no timeout)")

form_parser.add_option("auth_file", Default_auth_file, raw=True,
                       help="File with authentication tokens in JSON format (default: %s)" % Default_auth_file)

(options, args) = form_parser.parse_args()

Out_tty = sys.stdout.isatty() or options.widget

if not options.fullscreen and not options.csv and not options.json and not options.text:
    if Out_tty:
        options.fullscreen = True
    else:
        options.text = True

check_auth_file(options.auth_file)

params = {"scroll": "top", "current_directory": os.getcwd()}
if options.opacity:
        params["opacity"] = options.opacity

params["display"] = "fullscreen" if options.fullscreen else "block"

Headers = {"content_type": "text/html"}
Headers["x_gterm_response"] = "pagelet"
Headers["x_gterm_parameters"] = params

Json_headers = {"content_type": "text/json"}
Json_headers["x_gterm_response"] = "pagelet_json"
Json_headers["x_gterm_parameters"] = {}

if options.fullscreen:
    gterm.wrap_write(pagelet_html, headers=Headers, stderr=(not Out_tty))

if options.search or options.recv:
    try:
        read_twitter_user_stream(Auth_tokens, args, recv=options.recv)
    except KeyboardInterrupt:
        pass
else:
    update_status(" ".join(args))

if options.fullscreen:
    gterm.write_blank(exit_page=True, stderr=(not Out_tty))
