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