#!/usr/bin/ksh93

#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#

#
# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#

#
# shircbot - a simple IRC client/bot demo
#

# 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 fatal_error
{
	print -u2 "${progname}: $*"
	exit 1
}

# Definition for a IRC session class
typeset -T ircsession_t=(
	compound server=(
		typeset name
		integer port
	)
	
	typeset nick="ksh93irc"
	
	typeset running=true
	
	integer fd=-1
	
	function createsession
	{
		set -o xtrace
		
		_.server.name=$1
		_.server.port=$2
		_.nick=$3
		
		redirect {_.fd}<> "/dev/tcp/${_.server.name}/${_.server.port}"
		(( $? == 0 )) || { print -n2 $"Could not open server connection." ; return 1 ; }
		
		printf "fd=%d\n" _.fd
		
		return 0
	}

	function login
	{
		{
			printf "USER %s %s %s %s\n" "${_.nick}" "${_.nick}" "${_.nick}" "${_.nick}"
			printf "NICK %s\n" "${_.nick}"
		} >&${_.fd}
		
		return 0
	}

	function join_channel
	{
		printf "JOIN %s\n" "$1" >&${_.fd}
		
		return 0
	}
		
	function mainloop
	{
		typeset line
		float -S last_tick=0
		# We use the linebuf_t class here since network traffic
		# isn't guranteed to fit a single $'\n'-terminated line
		# into one TCP package. linebuf_t buffers characters
		# until it has one complete line. This avoids the need for
		# async I/O normally used by IRC clients
		linebuf_t serverbuf
		linebuf_t clientbuf
		integer fd=${_.fd}
	
		_.login
		
		while ${_.running} ; do
			while serverbuf.readbuf line <&${fd} ; do
				_.dispatch_serverevent "$line"
			done

			while clientbuf.readbuf line </dev/stdin ; do
				printf "client: %q\n" "${line}"
				printf "%s\n" "${line}" >&${fd}
			done
			
			# call mainloop_tick function in intervals to handle
			# async events (e.g. automatic /join etc.)
			if (( (SECONDS-last_tick) > 5. )) ; then
				(( last_tick=SECONDS ))
				_.mainloop_tick
			fi
		done
		
		return 0
	}
	
	function mainloop_tick
	{
		return 0
	}
	
	function dispatch_serverevent
	{
		typeset line="$1"
		
		case "${line}" in
			~(El)PING)
				compound ping_args=(
					line="$line"
				)
				_.serverevent_ping "ping_args"
				;;
			~(El):.*\ PRIVMSG)
				compound privmsg_args=(
					typeset line="$line"
					typeset msguser="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\1}"
					typeset msgchannel="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\3}"
					typeset msg="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\4}"
				)
				_.serverevent_privmsg "privmsg_args"
				;;
			~(El):.*\ INVITE)
				compound invite_args=(
					typeset line="$line"
					typeset inviteuser="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\1}"
					typeset invitenick="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\3}"
					typeset invitechannel="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\4}"
				)
				_.serverevent_invite "invite_args"
				;;
			*)
				printf "server: %q\n" "${line}"
				;;
		esac
		
		return 0
	}
	
	function serverevent_privmsg
	{
		nameref args=$1
		typeset msguser="${args.msguser}"
		typeset msgchannel="${args.msgchannel}"
		typeset msg="${args.msg}"
		
		printf "#privms: user=%q, channel=%q, msg=%q\n" "$msguser" "$msgchannel" "$msg"
		
		return 0
	}

	function serverevent_invite
	{
		nameref args=$1
		
		printf "JOIN %s\n" "${args.invitechannel/:/}" >&${_.fd}
		
		return 0
	}
		
	function send_privmsg
	{
		typeset channel="$1"
		typeset msg="$2"

		# Do we have to escape any characters in "msg" ?	
		printf "PRIVMSG %s :%s\n" "${channel}" "${msg}" >&${_.fd}

		return 0
	}
	
	function serverevent_ping
	{
		nameref args=$1

		printf "PONG %s\n" "${args.line/~(Elr)([^ ]+) ([^ ]+).*/\2}" >&${_.fd}

		return 0
	}
)

# line buffer class
# The buffer class tries to read characters from the given <fd> until
# it has read a whole line.
typeset -T linebuf_t=(
	typeset buf
	
	function reset
	{
		_.buf=""
		return 0
	}
	
	function readbuf
	{
		nameref var=$1
		typeset ch

		while IFS='' read -t 0.2 -N 1 ch ; do
			[[ "$ch" == $'\r' ]] && continue
			
			if [[ "$ch" == $'\n' ]] ; then
				var="${_.buf}"
				_.reset
				return 0
			fi
			
			_.buf+="$ch"
		done
		
		return 1
	}
)

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

# program start
# (be carefull with builtins here - they are unconditionally available
# in the shell's "restricted" mode)
builtin basename
builtin sum

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

typeset -r shircbot_usage=$'+
[-?\n@(#)\$Id: shircbot (Roland Mainz) 2009-09-09 \$\n]
[-author?Roland Mainz <roland.mainz@sun.com>]
[-author?Roland Mainz <roland.mainz@nrubsig.org>]
[+NAME?shircbot - simple IRC bot demo]
[+DESCRIPTION?\bshircbot\b is a small demo IRC bot which provides
	a simple IRC bot with several subcommands.]
[n:nickname?IRC nickname for this bot.]:[nick]
[s:ircserver?IRC servername.]:[servername]
[j:joinchannel?IRC servername.]:[channelname]
[+SEE ALSO?\bksh93\b(1)]
'

compound config=(
	typeset nickname="${LOGNAME}bot"
	typeset servername="irc.freenode.net"
	integer port=6667
	typeset -a join_channels
)

while getopts -a "${progname}" "${shircbot_usage}" OPT ; do 
#	printmsg "## OPT=|${OPT}|, OPTARG=|${OPTARG}|"
	case ${OPT} in
		n)	config.nickname="${OPTARG}" ;;
		s)	config.servername="${OPTARG}" ;;
		j)	config.join_channels+=( "${OPTARG}" ) ;;
		*)	usage ;;
	esac
done
shift $((OPTIND-1))

# if no channel was provided we join a predefined set of channels
if (( ${#config.join_channels[@]} == 0 )) ; then
	if [[ "${config.servername}" == "irc.freenode.net" ]] ; then
		config.join_channels+=( "#opensolaris" )
		config.join_channels+=( "#opensolaris-dev" )
		config.join_channels+=( "#opensolaris-arc" )
		config.join_channels+=( "#opensolaris-meeting" )
		config.join_channels+=( "#ospkg" )
		config.join_channels+=( "#ksh" )
	elif [[ "${config.servername}" == ~(E)irc.(sfbay|sweden) ]] ; then
		config.join_channels+=( "#onnv" )
	fi
fi

print "## Start."

ircsession_t mybot

# override ircsession_t::serverevent_privmsg with a new method for our bot
function mybot.serverevent_privmsg
{
	nameref args=$1
	typeset msguser="${args.msguser}"
	typeset msgchannel="${args.msgchannel}"
	typeset msg="${args.msg}"
	
	printf "#message: user=%q, channel=%q, msg=%q\n" "$msguser" "$msgchannel" "$msg"
	
	# Check if we get a private message
	if [[ "${msgchannel}" == "${_.nick}" ]] ; then
		# ${msgchannel} point to our own nick if we got a private message,
		# we need to extract the sender's nickname from ${msguser} and put
		# it into msgchannel
		msgchannel="${msguser/~(El):(.*)!.*/\1}"
	else
		# check if this is a command for this bot
		[[ "$msg" != ~(Eli):${_.nick}:[[:space:]]  ]] && return 0
	fi
	
	# strip beginning (e.g. ":<nick>:" or ":") plus extra spaces
	msg="${msg/~(Eli)(:${_.nick})*:[[:space:]]*/}"
	
	printf "botmsg=%q\n" "$msg"
	
	case "$msg" in
		~(Eli)date)
			_.send_privmsg "$msgchannel" "${
			        printf "%(%Y-%m-%d, %Th/%Z)T\n"
			}"
			;;
		~(Eli)echo)
			_.send_privmsg "$msgchannel" "${msg#*echo}"
			;;
		~(Eli)exitbot)
			typeset exitkey="$(print "$msguser" | sum -x sha1)" # this is unwise&&insecure
			if [[ "$msg" == *${exitkey}* ]] ; then
				_.running=false
			fi
			;;
		~(Eli)help)
			_.send_privmsg "$msgchannel" "${
				printf "Hello, this is shircbot, written in ksh93 (%s). " "${.sh.version}"
				printf "Subcommands are 'say hello', 'math <math-expr>', 'stocks', 'uuid', 'date' and 'echo'."
				}"
			;;
		~(Eli)math)
			if [[ "${msg}" == ~(E)[\`\$] ]] ; then
				# "restricted" shell mode would prevent any damage but we try to be carefull...
				_.send_privmsg "$msgchannel" "Syntax error."
			else
				typeset mathexpr="${msg#*math}"

				printf "Calculating '%s'\n" "${mathexpr}"
				_.send_privmsg "$msgchannel" "${
				        ( printf 'export PATH=/usr/${RANDOM}/$$/${RANDOM}/foo ; set -o restricted ; printf "%%s = %%.40g\n" "%s" $(( %s ))\n' "${mathexpr}" "${mathexpr}" | source /dev/stdin 2>&1 )
				}"
			fi
			;;
		~(Eli)say\ hello)
			_.send_privmsg "$msgchannel" "Hello, this is a bot."
			;;
		~(Eli)stocks)
			typeset stockmsg tickersymbol
			for tickersymbol in "JAVA" "ORCL" "IBM" "AAPL" "HPQ" ; do
				stockmsg="$( /usr/sfw/bin/wget -q -O /dev/stdout "http://quote.yahoo.com/d/quotes.csv?f=sl1d1t1c1ohgv&e=.csv&s=${tickersymbol}" 2>&1 )"
				_.send_privmsg "$msgchannel" "${tickersymbol}: ${stockmsg//,/ }"
			done
			;;
		~(Eli)uuid)
			_.send_privmsg "$msgchannel" "${
			        print "%(%Y%M%D%S%N)T$((RANDOM))%s\n" "${msguser}" | sum -x sha256
			}"
			;;
	esac
	
	return 0
}

# Automatically join the list of channels listed in |config.join_channels|
# after the client is connected to the server for some time
function mybot.mainloop_tick
{
	integer -S autojoin_done=2
	integer i
	
	if (( autojoin_done-- == 0 && ${#config.join_channels[@]} > 0 )) ; then
		print "# Autojoin channels..."

		for ((i=0 ; i < ${#config.join_channels[@]} ; i++ )) ; do
			mybot.join_channel "${config.join_channels[i]}"
		done
	fi
	
	return 0
}

mybot.createsession "${config.servername}" ${config.port} "${config.nickname}"

# This is a network-facing application - once we've set eveything up
# we set PATH to a random value and switch to the shell's restricted
# mode to make sure noone can escape the jail.
#export PATH=/usr/$RANDOM/foo
#set -o restricted

mybot.mainloop

print "## End."

exit 0