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