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 2009 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 compound 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 _.login 116 117 while ${_.running} ; do 118 while serverbuf.readbuf line <&${fd} ; do 119 _.dispatch_serverevent "$line" 120 done 121 122 while clientbuf.readbuf line </dev/stdin ; do 123 printf "client: %q\n" "${line}" 124 printf "%s\n" "${line}" >&${fd} 125 done 126 127 # call mainloop_tick function in intervals to handle 128 # async events (e.g. automatic /join etc.) 129 if (( (SECONDS-last_tick) > 5. )) ; then 130 (( last_tick=SECONDS )) 131 _.mainloop_tick 132 fi 133 done 134 135 return 0 136 } 137 138 function mainloop_tick 139 { 140 return 0 141 } 142 143 function dispatch_serverevent 144 { 145 typeset line="$1" 146 147 case "${line}" in 148 ~(El)PING) 149 compound ping_args=( 150 line="$line" 151 ) 152 _.serverevent_ping "ping_args" 153 ;; 154 ~(El):.*\ PRIVMSG) 155 compound privmsg_args=( 156 typeset line="$line" 157 typeset msguser="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\1}" 158 typeset msgchannel="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\3}" 159 typeset msg="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\4}" 160 ) 161 _.serverevent_privmsg "privmsg_args" 162 ;; 163 ~(El):.*\ INVITE) 164 compound invite_args=( 165 typeset line="$line" 166 typeset inviteuser="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\1}" 167 typeset invitenick="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\3}" 168 typeset invitechannel="${line/~(Elr)([^ ]+) ([^ ]+) ([^ ]+) (.*)/\4}" 169 ) 170 _.serverevent_invite "invite_args" 171 ;; 172 *) 173 printf "server: %q\n" "${line}" 174 ;; 175 esac 176 177 return 0 178 } 179 180 function serverevent_privmsg 181 { 182 nameref args=$1 183 typeset msguser="${args.msguser}" 184 typeset msgchannel="${args.msgchannel}" 185 typeset msg="${args.msg}" 186 187 printf "#privms: user=%q, channel=%q, msg=%q\n" "$msguser" "$msgchannel" "$msg" 188 189 return 0 190 } 191 192 function serverevent_invite 193 { 194 nameref args=$1 195 196 printf "JOIN %s\n" "${args.invitechannel/:/}" >&${_.fd} 197 198 return 0 199 } 200 201 function send_privmsg 202 { 203 typeset channel="$1" 204 typeset msg="$2" 205 206 # Do we have to escape any characters in "msg" ? 207 printf "PRIVMSG %s :%s\n" "${channel}" "${msg}" >&${_.fd} 208 209 return 0 210 } 211 212 function serverevent_ping 213 { 214 nameref args=$1 215 216 printf "PONG %s\n" "${args.line/~(Elr)([^ ]+) ([^ ]+).*/\2}" >&${_.fd} 217 218 return 0 219 } 220) 221 222# line buffer class 223# The buffer class tries to read characters from the given <fd> until 224# it has read a whole line. 225typeset -T linebuf_t=( 226 typeset buf 227 228 function reset 229 { 230 _.buf="" 231 return 0 232 } 233 234 function readbuf 235 { 236 nameref var=$1 237 typeset ch 238 239 while IFS='' read -t 0.2 -N 1 ch ; do 240 [[ "$ch" == $'\r' ]] && continue 241 242 if [[ "$ch" == $'\n' ]] ; then 243 var="${_.buf}" 244 _.reset 245 return 0 246 fi 247 248 _.buf+="$ch" 249 done 250 251 return 1 252 } 253) 254 255function usage 256{ 257 OPTIND=0 258 getopts -a "${progname}" "${shircbot_usage}" OPT '-?' 259 exit 2 260} 261 262# program start 263# (be carefull with builtins here - they are unconditionally available 264# in the shell's "restricted" mode) 265builtin basename 266builtin sum 267 268typeset progname="${ basename "${0}" ; }" 269 270typeset -r shircbot_usage=$'+ 271[-?\n@(#)\$Id: shircbot (Roland Mainz) 2009-09-09 \$\n] 272[-author?Roland Mainz <roland.mainz@sun.com>] 273[-author?Roland Mainz <roland.mainz@nrubsig.org>] 274[+NAME?shircbot - simple IRC bot demo] 275[+DESCRIPTION?\bshircbot\b is a small demo IRC bot which provides 276 a simple IRC bot with several subcommands.] 277[n:nickname?IRC nickname for this bot.]:[nick] 278[s:ircserver?IRC servername.]:[servername] 279[j:joinchannel?IRC servername.]:[channelname] 280[+SEE ALSO?\bksh93\b(1)] 281' 282 283compound config=( 284 typeset nickname="${LOGNAME}bot" 285 typeset servername="irc.freenode.net" 286 integer port=6667 287 typeset -a join_channels 288) 289 290while getopts -a "${progname}" "${shircbot_usage}" OPT ; do 291# printmsg "## OPT=|${OPT}|, OPTARG=|${OPTARG}|" 292 case ${OPT} in 293 n) config.nickname="${OPTARG}" ;; 294 s) config.servername="${OPTARG}" ;; 295 j) config.join_channels+=( "${OPTARG}" ) ;; 296 *) usage ;; 297 esac 298done 299shift $((OPTIND-1)) 300 301# if no channel was provided we join a predefined set of channels 302if (( ${#config.join_channels[@]} == 0 )) ; then 303 if [[ "${config.servername}" == "irc.freenode.net" ]] ; then 304 config.join_channels+=( "#opensolaris" ) 305 config.join_channels+=( "#opensolaris-dev" ) 306 config.join_channels+=( "#opensolaris-arc" ) 307 config.join_channels+=( "#opensolaris-meeting" ) 308 config.join_channels+=( "#ospkg" ) 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}/$$/${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" "ORCL" "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