#!/usr/bin/ksh93

########################################################################
#                                                                      #
#               This software is part of the ast package               #
#                 Copyright (c) 2008-2012 Roland Mainz                 #
#                      and is licensed under the                       #
#                 Eclipse Public License, Version 1.0                  #
#                    by AT&T Intellectual Property                     #
#                                                                      #
#                A copy of the License is available at                 #
#          http://www.eclipse.org/org/documents/epl-v10.html           #
#         (with md5 checksum b35adb5213ca9657e911e9befb180842)         #
#                                                                      #
#                                                                      #
#                 Roland Mainz <roland.mainz@nrubsig.org>              #
#                                                                      #
########################################################################

#
# Copyright (c) 2008, 2011, Roland Mainz. All rights reserved.
#

#
# Written by Roland Mainz <roland.mainz@nrubsig.org>
#

# Solaris needs /usr/xpg6/bin:/usr/xpg4/bin because the tools in /usr/bin are not POSIX-conformant
export PATH='/usr/xpg6/bin:/usr/xpg4/bin:/bin:/usr/bin'

# Make sure all math stuff runs in the "C" locale to avoid problems
# with alternative # radix point representations (e.g. ',' instead of
# '.' in de_DE.*-locales). This needs to be set _before_ any
# floating-point constants are defined in this script).
if [[ "${LC_ALL-}" != '' ]] ; then
	export \
		LC_MONETARY="${LC_ALL}" \
		LC_MESSAGES="${LC_ALL}" \
		LC_COLLATE="${LC_ALL}" \
		LC_CTYPE="${LC_ALL}"
		unset LC_ALL
fi
export LC_NUMERIC='C'


function urlencode_string
{
	nameref outbuf="$1"
	typeset inbuf="$2"
	typeset c
	typeset buf=''
	integer i
	integer inbuflen=${#inbuf}

	for (( i=0 ; i < inbuflen ; i++ )) ; do
		c="${inbuf:i:1}"
		case "${c}" in
			' ') c='+'   ;;
			'!') c='%21' ;;
			'*') c='%2A' ;;
			"'") c='%27' ;;
			'(') c='%28' ;;
			')') c='%29' ;;
			';') c='%3B' ;;
			':') c='%3A' ;;
			'@') c='%40' ;;
			'&') c='%26' ;;
			'=') c='%3D' ;;
			'+') c='%2B' ;;
			'$') c='%24' ;;
			',') c='%2C' ;;
			'/') c='%2F' ;;
			'?') c='%3F' ;;
			'%') c='%25' ;;
			'#') c='%23' ;;
			'[') c='%5B' ;;
			'\') c='%5C' ;; # we need this to avoid the '\'-quoting hell
			']') c='%5D' ;;
			*)   ;;
		esac
		buf+="${c}"
	done
	
	outbuf="${buf}"
	
	return 0
}

function encode_x_www_form_urlencoded
{
	nameref formdata=$1
	nameref content='formdata.content'
	integer numformelements=${#formdata.form[*]}
	integer i
    
	content=''
    
	for (( i=0 ; i < numformelements ; i++ )) ; do
		nameref element="formdata.form[${i}]"
		typeset data="${element.data}"
		typeset buf

		[[ "${content}" != '' ]] && content+='&'
	
		content+="${element.name}="

		urlencode_string buf "${data}"
		content+="${buf}"        
	done
    
	formdata.content_length=${#content}

	return 0
}

# parse HTTP return code, cookies etc.
function parse_http_response
{
	nameref response="$1"
	typeset h statuscode statusmsg s
	integer i
    
	# we use '\r' as additional IFS to filter the final '\r'
	IFS=$' \t\r' read -r h statuscode statusmsg  # read HTTP/1.[01] <code>
	[[ "${h}" != ~(Eil)HTTP/ ]]           && { print -u2 -f $"%s: HTTP/ header missing\n" "$0" ; return 1 ; }
	[[ "${statuscode}" != ~(Elr)[0-9]+ ]] && { print -u2 -f $"%s: invalid status code\n"  "$0" ; return 1 ; }

	integer response.statuscode="10#${statuscode}"
	typeset response.statusmsg="${statusmsg}"
    
    	typeset -a response.headers
	
	# collect headers
	while IFS='' read -r s ; do
		[[ "${s}" == $'\r' ]] && break

		# strip '\r' at the end
		s="${s/~(Er)$'\r'/}"
		
		response.headers+=( "${s}" )
	done

	for (( i=0 ; i < ${#response.headers[@]} ; i++ )) ; do
		s="${response.headers[i]}"
		# add compound variable fields _ONLY_ on _demand_ if the
		# matching headers exist
		case "${s}" in
			~(Eli)Content-Length:[[:blank:]]+[0-9]+)
				integer response.content_length="10#${s/~(Eli)Content-Length:[[:blank:]]+/}"
				;;
			~(Eli)Content-Type:[[:blank:]]+)
				typeset response.content_type="${s/~(Eli)Content-Type:[[:blank:]]+/}"
				;;
			~(Eli)Location:[[:blank:]]+)
				typeset response.location="${s/~(Eli)Location:[[:blank:]]+/}"
				;;
			~(Eli)Transfer-Encoding:[[:blank:]]+)
				typeset response.transfer_encoding="${s/~(Eli)Transfer-Encoding:[[:blank:]]+/}"
				;;
		esac
	done
	
	return 0
}

function cat_http_body
{
	typeset emode="$1"
	typeset hexchunksize='0'
	integer chunksize=0

	if [[ "${emode}" == 'chunked' ]] ; then
		while IFS=$'\n' read hexchunksize ; do
			hexchunksize="${hexchunksize//$'\r'/}"
			[[ "${hexchunksize}" != '' ]] || continue
			[[ "${hexchunksize}" == ~(Elr)[[:xdigit:]]+ ]] || break
			chunksize="16#${hexchunksize}"
			(( chunksize > 0 )) || break
			dd bs=1 count="${chunksize}" 2>'/dev/null'
		done
	else
		cat
	fi

	return 0
}

function encode_http_basic_auth
{
	typeset user="$1"
	typeset passwd="$2"
	typeset s
	integer s_len
	typeset -b base64var
    
	# ksh93 binary variables use base64 encoding, the same as the
	# HTTP basic authentification. We only have to read the
	# plaintext user:passwd string into the binary variable "base64var"
	# and then print this variable as ASCII.
	s="${user}:${passwd}"
	s_len="${ wc --bytes <<<"${s}" ; }"
	(( s_len-- ))
	print -r -n "${s}" | read -N${s_len} base64var
    
	printf '%s\n' "${base64var}" # print ASCII (base64) representation of binary var
    
	return 0
}

function put_twitter_message
{
	[[ -v SHTWITTER_USER   ]] || { print -u2 -f $"%s: SHTWITTER_USER not set.\n" "$0" ; return 1 ; }
	[[ -v SHTWITTER_PASSWD ]] || { print -u2 -f $"%s: SHTWITTER_PASSWD not set.\n" "$0" ; return 1 ; }

	(( $# != 1 )) && { print -u2 -f $"%s: Wrong number of arguments.\n" "$0" ; return 1 ; }

	# site setup
	typeset url_host='twitter.com'
	typeset url_path='/statuses/update.xml'
	typeset url="http://${url_host}${url_path}"
	integer netfd # http stream number
	typeset msgtext="$1"
	compound httpresponse # http response
	integer res

	# argument for "encode_x_www_form_urlencoded"
	compound urlform=(
		# input
		compound -a form=(
			( name='status'	data="${msgtext}" )
		)
		# output
		typeset content
		integer content_length
	)
     
	typeset request=''
	typeset content=''

	encode_x_www_form_urlencoded urlform
          
	content="${urlform.content}"

	request="POST ${url_path} HTTP/1.1\r\n"
	request+="Host: ${url_host}\r\n"
	request+="Authorization: Basic ${ encode_http_basic_auth "${SHTWITTER_USER}" "${SHTWITTER_PASSWD}" ; }\r\n"
	request+="User-Agent: ${http_user_agent}\r\n"
	request+='Connection: close\r\n'
	request+='Content-Type: application/x-www-form-urlencoded\r\n'
	request+="Content-Length: $(( urlform.content_length ))\r\n"

	redirect {netfd}<> "/dev/tcp/${url_host}/80" 
	(( $? != 0 )) && { print -u2 -f $"%s: Could not open connection to %s\n." "$0" "${url_host}" ;  return 1 ; }

	# send http post
	{
		print -n -- "${request}\r\n"
		print -n -- "${content}\r\n"
	}  >&${netfd}

        # process reply
        parse_http_response httpresponse <&${netfd} ; (( res=$? ))
        if (( res == 0 )) ; then
                response="${ cat_http_body "${httpresponse.transfer_encoding-}" <&${netfd} ; }" ; (( res+=$? ))
        fi

	# close connection
	redirect {netfd}<&-
    
	printf $"twitter response was (%s,%s): %s\n" "${httpresponse.statuscode}" "${httpresponse.statusmsg}" "${response}"
    
	if (( res == 0 && httpresponse.statuscode >= 200 && httpresponse.statuscode <= 299 )) ; then
		return 0
	else
		return 1
	fi

	# not reached
}

function verify_twitter_credentials
{
	[[ -v SHTWITTER_USER   ]] || { print -u2 -f $"%s: SHTWITTER_USER not set.\n" "$0" ; return 1 ; }
	[[ -v SHTWITTER_PASSWD ]] || { print -u2 -f $"%s: SHTWITTER_PASSWD not set.\n" "$0" ; return 1 ; }

	(( $# != 0 )) && { print -u2 -f $"%s: Wrong number of arguments.\n" "$0" ; return 1 ; }

	# site setup
	typeset url_host='twitter.com'
	typeset url_path='/account/verify_credentials.xml'
	typeset url="http://${url_host}${url_path}"
	integer netfd # http stream number
	compound httpresponse # http response

	typeset request=''

	request="GET ${url_path} HTTP/1.1\r\n"
	request+="Host: ${url_host}\r\n"
	request+="Authorization: Basic ${ encode_http_basic_auth "${SHTWITTER_USER}" "${SHTWITTER_PASSWD}" ; }\r\n"
	request+="User-Agent: ${http_user_agent}\r\n"
	request+='Connection: close\r\n'

	redirect {netfd}<> "/dev/tcp/${url_host}/80" 
	(( $? != 0 )) && { print -u2 -f $"%s: Could not open connection to %s.\n" "$0" "${url_host}" ;  return 1 ; }

	# send http post
	{
		print -n -- "${request}\r\n"
	}  >&${netfd}

	# process reply
	parse_http_response httpresponse <&${netfd} ; (( res=$? ))
	if (( res == 0 )) ; then
		response="${ cat_http_body "${httpresponse.transfer_encoding-}" <&${netfd} ; }" ; (( res+=$? ))
	fi

	# close connection
	redirect {netfd}<&-
    
	printf $"twitter response was (%s,%s): %s\n" "${httpresponse.statuscode}" "${httpresponse.statusmsg}" "${response}"
    
	if (( res == 0 && httpresponse.statuscode >= 200 && httpresponse.statuscode <= 299 )) ; then
		return 0
	else
		return 1
	fi

	# not reached
}

function usage
{
	OPTIND=0
	getopts -a "${progname}" "${shtwitter_usage}" OPT '-?'
	exit 2
}

function main
{
	typeset method

	while getopts -a "${progname}" "${shtwitter_usage}" OPT ; do 
		case "${OPT}" in
			*)	usage ;;
		esac
	done
	shift $(( OPTIND-1 ))
	
	# expecting at least one more argument
	(( $# >= 1 )) || usage
	
	method="$1"
	shift
	
	case "${method}" in
		'update' | 'blog')	put_twitter_message        "$@" ; return $? ;;
		'verify_credentials')	verify_twitter_credentials "$@" ; return $? ;;
		*)			usage ;;
	esac

	return 0
}

# program start
builtin basename
builtin cat
builtin date
builtin uname
builtin wc

set -o noglob
set -o nounset

typeset progname="${ basename "${0}" ; }"

# HTTP protocol client identifer
typeset -r http_user_agent="shtwitter/ksh93 (2010-05-17; ${ uname -s -r -p ; })"

typeset -r shtwitter_usage=$'+
[-?\n@(#)\$Id: shtwitter (Roland Mainz) 2010-05-17 \$\n]
[-author?Roland Mainz <roland.mainz@nrubsig.org>]
[+NAME?shtwitter - read/write text data to internet clipboards]
[+DESCRIPTION?\bshtwitter\b is a small utility which can read and write text
	to the twitter.com microblogging site.]
[+?The first arg \bmethod\b describes one of the methods, "update" posts a
	text message to the users twitter blog, returning the raw response
	message from the twitter server.]
[+?The second arg \bstring\b contains the string data which should be
	stored on twitter.com.]

method [ string ]

[+SEE ALSO?\bksh93\b(1), \brssread\b(1), \bshtinyurl\b(1), http://www.twitter.com]
'

main "$@"
exit $?

# EOF.
