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