xref: /titanic_44/usr/src/lib/libshell/common/scripts/shircbot.sh (revision db8b037b5616a366b7dfdc01ef9552f02f9adfdd)
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