#!/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 &${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 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 ] [-author?Roland Mainz ] [+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. "::" 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 ', '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