1#!/usr/bin/ksh93 2 3# 4# CDDL HEADER START 5# 6# The contents of this file are subject to the terms of the 7# Common Development and Distribution License (the "License"). 8# You may not use this file except in compliance with the License. 9# 10# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 11# or http://www.opensolaris.org/os/licensing. 12# See the License for the specific language governing permissions 13# and limitations under the License. 14# 15# When distributing Covered Code, include this CDDL HEADER in each 16# file and include the License file at usr/src/OPENSOLARIS.LICENSE. 17# If applicable, add the following below this CDDL HEADER, with the 18# fields enclosed by brackets "[]" replaced with your own identifying 19# information: Portions Copyright [yyyy] [name of copyright owner] 20# 21# CDDL HEADER END 22# 23 24# 25# Copyright 2008 Sun Microsystems, Inc. All rights reserved. 26# Use is subject to license terms. 27# 28 29# 30# shircbot - a simple IRC client/bot demo 31# 32 33# Solaris needs /usr/xpg6/bin:/usr/xpg4/bin because the tools in /usr/bin are not POSIX-conformant 34export PATH=/usr/xpg6/bin:/usr/xpg4/bin:/bin:/usr/bin 35 36# Make sure all math stuff runs in the "C" locale to avoid problems 37# with alternative # radix point representations (e.g. ',' instead of 38# '.' in de_DE.*-locales). This needs to be set _before_ any 39# floating-point constants are defined in this script). 40if [[ "${LC_ALL}" != "" ]] ; then 41 export \ 42 LC_MONETARY="${LC_ALL}" \ 43 LC_MESSAGES="${LC_ALL}" \ 44 LC_COLLATE="${LC_ALL}" \ 45 LC_CTYPE="${LC_ALL}" 46 unset LC_ALL 47fi 48export LC_NUMERIC=C 49 50function fatal_error 51{ 52 print -u2 "${progname}: $*" 53 exit 1 54} 55 56# Definition for a IRC session class 57typeset -T ircsession_t=( 58 typeset -C server=( 59 typeset name 60 integer port 61 ) 62 63 typeset nick="ksh93irc" 64 65 typeset running=true 66 67 integer fd=-1 68 69 function createsession 70 { 71 set -o xtrace 72 73 _.server.name=$1 74 _.server.port=$2 75 _.nick=$3 76 77 redirect {_.fd}<>"/dev/tcp/${_.server.name}/${_.server.port}" 78 (( $? == 0 )) || { print -n2 $"Could not open server connection." ; return 1 ; } 79 80 printf "fd=%d\n" _.fd 81 82 return 0 83 } 84 85 function login 86 { 87 { 88 printf "USER %s %s %s %s\n" "${_.nick}" "${_.nick}" "${_.nick}" "${_.nick}" 89 printf "NICK %s\n" "${_.nick}" 90 } >&${_.fd} 91 92 return 0 93 } 94 95 function join_channel 96 { 97 printf "JOIN %s\n" "$1" >&${_.fd} 98 99 return 0 100 } 101 102 function mainloop 103 { 104 typeset line 105 float -S last_tick=0 106 # We use the linebuf_t class here since network traffic 107 # isn't guranteed to fit a single $'\n'-terminated line 108 # into one TCP package. linebuf_t buffers characters 109 # until it has one complete line. This avoids the need for 110 # async I/O normally used by IRC clients 111 linebuf_t serverbuf 112 linebuf_t clientbuf 113 integer fd=${_.fd} 114 115 set -o xtrace 116 117 _.login 118 119 while ${_.running} ; do 120 while serverbuf.readbuf line <&${fd} ; do 121 _.dispatch_serverevent "$line" 122 done 123 124 while clientbuf.readbuf line </dev/stdin ; do 125 printf "client: %q\n" "${line}" 126 printf "%s\n" "${line}" >&${fd} 127 done 128 129 # call mainloop_tick function in intervals to handle 130 # async events (e.g. automatic /join etc.) 131 if (( (SECONDS-last_tick) > 5. )) ; then 132 (( last_tick=SECONDS )) 133 _.mainloop_tick 134 fi 135 done 136 137 return 0 138 } 139 140 function mainloop_tick 141 { 142 return 0 143 } 144 145 function dispatch_serverevent 146 { 147 typeset line="$1" 148 149 case "${line}" in 150 ~(El)PING) 151 typeset -C ping_args=( 152 line="$line" 153 ) 154 _.serverevent_ping "ping_args" 155 ;; 156 ~(El):.*\ PRIVMSG) 157 typeset -C privmsg_args=( 158 typeset line="$line" 159 typeset msguser="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\1}" 160 typeset msgchannel="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\3}" 161 typeset msg="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\4}" 162 ) 163 _.serverevent_privmsg "privmsg_args" 164 ;; 165 ~(El):.*\ INVITE) 166 typeset -C invite_args=( 167 typeset line="$line" 168 typeset inviteuser="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\1}" 169 typeset invitenick="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\3}" 170 typeset invitechannel="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\4}" 171 ) 172 _.serverevent_invite "invite_args" 173 ;; 174 *) 175 printf "server: %q\n" "${line}" 176 ;; 177 esac 178 179 return 0 180 } 181 182 function serverevent_privmsg 183 { 184 nameref args=$1 185 typeset msguser="${args.msguser}" 186 typeset msgchannel="${args.msgchannel}" 187 typeset msg="${args.msg}" 188 189 printf "#privms: user=%q, channel=%q, msg=%q\n" "$msguser" "$msgchannel" "$msg" 190 191 return 0 192 } 193 194 function serverevent_invite 195 { 196 nameref args=$1 197 198 printf "JOIN %s\n" "${args.invitechannel/:/}" >&${_.fd} 199 200 return 0 201 } 202 203 function send_privmsg 204 { 205 typeset channel="$1" 206 typeset msg="$2" 207 208 # Do we have to escape any characters in "msg" ? 209 printf "PRIVMSG %s :%s\n" "${channel}" "${msg}" >&${_.fd} 210 211 return 0 212 } 213 214 function serverevent_ping 215 { 216 nameref args=$1 217 218 printf "PONG %s\n" "${args.line/~(Elr)([^ ]+) ([^ ]+).*/\2}" >&${_.fd} 219 220 return 0 221 } 222) 223 224# line buffer class 225# The buffer class tries to read characters from the given <fd> until 226# it has read a whole line. 227typeset -T linebuf_t=( 228 typeset buf 229 230 function reset 231 { 232 _.buf="" 233 return 0 234 } 235 236 function readbuf 237 { 238 nameref var=$1 239 typeset ch 240 241 while IFS='' read -t 0.2 -N 1 ch ; do 242 [[ "$ch" == $'\r' ]] && continue 243 244 if [[ "$ch" == $'\n' ]] ; then 245 var="${_.buf}" 246 _.reset 247 return 0 248 fi 249 250 _.buf+="$ch" 251 done 252 253 return 1 254 } 255) 256 257function usage 258{ 259 OPTIND=0 260 getopts -a "${progname}" "${shircbot_usage}" OPT '-?' 261 exit 2 262} 263 264# program start 265# (be carefull with builtins here - they are unconditionally available 266# in the shell's "restricted" mode) 267builtin basename 268builtin sum 269 270typeset progname="${ basename "${0}" ; }" 271 272typeset -r shircbot_usage=$'+ 273[-?\n@(#)\$Id: shircbot (Roland Mainz) 2008-10-31 \$\n] 274[-author?Roland Mainz <roland.mainz@sun.com>] 275[-author?Roland Mainz <roland.mainz@nrubsig.org>] 276[+NAME?shircbot - simple IRC bot demo] 277[+DESCRIPTION?\bshircbot\b is a small demo IRC bot which provides 278 a simple IRC bot with several subcommands.] 279[n:nickname?IRC nickname for this bot.]:[nick] 280[s:ircserver?IRC servername.]:[servername] 281[j:joinchannel?IRC servername.]:[channelname] 282[+SEE ALSO?\bksh93\b(1)] 283' 284 285typeset -C config=( 286 typeset nickname="${LOGNAME}bot" 287 typeset servername="irc.freenode.net" 288 integer port=6667 289 typeset -a join_channels 290) 291 292while getopts -a "${progname}" "${shircbot_usage}" OPT ; do 293# printmsg "## OPT=|${OPT}|, OPTARG=|${OPTARG}|" 294 case ${OPT} in 295 n) config.nickname="${OPTARG}" ;; 296 s) config.servername="${OPTARG}" ;; 297 j) config.join_channels+=( "${OPTARG}" ) ;; 298 *) usage ;; 299 esac 300done 301shift $((OPTIND-1)) 302 303# if no channel was provided we join a predefined set of channels 304if (( ${#config.join_channels[@]} == 0 )) ; then 305 if [[ "${config.servername}" == "irc.freenode.net" ]] ; then 306 config.join_channels+=( "#opensolaris" ) 307 config.join_channels+=( "#opensolaris-dev" ) 308 config.join_channels+=( "#opensolaris-arc" ) 309 config.join_channels+=( "#ksh" ) 310 elif [[ "${config.servername}" == ~(E)irc.(sfbay|sweden) ]] ; then 311 config.join_channels+=( "#onnv" ) 312 fi 313fi 314 315print "## Start." 316 317ircsession_t mybot 318 319# override ircsession_t::serverevent_privmsg with a new method for our bot 320function mybot.serverevent_privmsg 321{ 322 nameref args=$1 323 typeset msguser="${args.msguser}" 324 typeset msgchannel="${args.msgchannel}" 325 typeset msg="${args.msg}" 326 327 printf "#message: user=%q, channel=%q, msg=%q\n" "$msguser" "$msgchannel" "$msg" 328 329 # Check if we get a private message 330 if [[ "${msgchannel}" == "${_.nick}" ]] ; then 331 # ${msgchannel} point to our own nick if we got a private message, 332 # we need to extract the sender's nickname from ${msguser} and put 333 # it into msgchannel 334 msgchannel="${msguser/~(El):(.*)!.*/\1}" 335 else 336 # check if this is a command for this bot 337 [[ "$msg" != ~(Eli):${_.nick}:[[:space:]] ]] && return 0 338 fi 339 340 # strip beginning (e.g. ":<nick>:" or ":") plus extra spaces 341 msg="${msg/~(Eli)(:${_.nick})*:[[:space:]]*/}" 342 343 printf "botmsg=%q\n" "$msg" 344 345 case "$msg" in 346 ~(Eli)date) 347 _.send_privmsg "$msgchannel" "$( 348 ( printf "%(%Y-%m-%d, %Th/%Z)T\n" ) 349 )" 350 ;; 351 ~(Eli)echo) 352 _.send_privmsg "$msgchannel" "${msg#*echo}" 353 ;; 354 ~(Eli)exitbot) 355 typeset exitkey="$(print "$msguser" | sum -x sha1)" # this is unwise&&insecure 356 if [[ "$msg" == *${exitkey}* ]] ; then 357 _.running=false 358 fi 359 ;; 360 ~(Eli)help) 361 _.send_privmsg "$msgchannel" "$( 362 printf "Hello, this is shircbot, written in ksh93 (%s). " "${.sh.version}" 363 printf "Subcommands are 'say hello', 'math <math-expr>', 'stocks', 'uuid', 'date' and 'echo'." 364 )" 365 ;; 366 ~(Eli)math) 367 if [[ "${msg}" == ~(E)[\`\$] ]] ; then 368 # "restricted" shell mode would prevent any damage but we try to be carefull... 369 _.send_privmsg "$msgchannel" "Syntax error." 370 else 371 typeset mathexpr="${msg#*math}" 372 373 printf "Calculating '%s'\n" "${mathexpr}" 374 _.send_privmsg "$msgchannel" "$( 375 ( printf 'export PATH=/usr/$RANDOM/foo ; set -o restricted ; printf "%%s = %%.40g\n" "%s" $(( %s ))\n' "${mathexpr}" "${mathexpr}" | source /dev/stdin 2>&1 ) 376 )" 377 fi 378 ;; 379 ~(Eli)say\ hello) 380 _.send_privmsg "$msgchannel" "Hello, this is a bot." 381 ;; 382 ~(Eli)stocks) 383 typeset stockmsg tickersymbol 384 for tickersymbol in "JAVA" "IBM" "AAPL" "HPQ" ; do 385 stockmsg="$( /usr/sfw/bin/wget -q -O /dev/stdout "http://quote.yahoo.com/d/quotes.csv?f=sl1d1t1c1ohgv&e=.csv&s=${tickersymbol}" 2>&1 )" 386 _.send_privmsg "$msgchannel" "${tickersymbol}: ${stockmsg//,/ }" 387 done 388 ;; 389 ~(Eli)uuid) 390 _.send_privmsg "$msgchannel" "$( 391 ( print "%(%Y%M%D%S%N)T$((RANDOM))%s\n" "${msguser}" | sum -x sha256 ) 392 )" 393 ;; 394 esac 395 396 return 0 397} 398 399# Automatically join the list of channels listed in |config.join_channels| 400# after the client is connected to the server for some time 401function mybot.mainloop_tick 402{ 403 integer -S autojoin_done=2 404 integer i 405 406 if (( autojoin_done-- == 0 && ${#config.join_channels[@]} > 0 )) ; then 407 print "# Autojoin channels..." 408 409 for ((i=0 ; i < ${#config.join_channels[@]} ; i++ )) ; do 410 mybot.join_channel "${config.join_channels[i]}" 411 done 412 fi 413 414 return 0 415} 416 417mybot.createsession "${config.servername}" ${config.port} "${config.nickname}" 418 419# This is a network-facing application - once we've set eveything up 420# we set PATH to a random value and switch to the shell's restricted 421# mode to make sure noone can escape the jail. 422#export PATH=/usr/$RANDOM/foo 423#set -o restricted 424 425mybot.mainloop 426 427print "## End." 428 429exit 0 430