mirror of https://tildegit.org/ben/sedbot
414 lines
8.2 KiB
Bash
Executable File
414 lines
8.2 KiB
Bash
Executable File
#!/bin/bash
|
|
#sedbot: a regex IRC bot in bash and sed
|
|
#license: GNU GPL v3+
|
|
|
|
|
|
SERVER=irc.example.net # the IRC server to connect to
|
|
PORT=6667 # port to connect to
|
|
NICK=sedbot # bot's nickname
|
|
LOGIN=sedbot # bot's username
|
|
REALNAME=sedbot # bot's name
|
|
CHANNELS=('#example') # array of channels to autojoin at start
|
|
IRC_LOG=~/sedbot.log # irc message log
|
|
ERROR_LOG=~/sedbot.err # log of errors, events and used regexps
|
|
|
|
MAX_LINE_LENGTH=400 # truncate lines to this many bytes
|
|
SLEEP_JOIN=3 # sleep after identifying before autojoining channels
|
|
SLEEP_RECONNECT=10 # sleep before reconnecting on disconnect
|
|
READ_TIMEOUT=300 # seconds before reconnect if no line is read
|
|
ACCEPT_INVITES=1 # 0 to ignore invites
|
|
|
|
|
|
###############################################################################
|
|
|
|
|
|
exec 4> >(tee -a -- "$IRC_LOG") 2> >(tee -a -- "$ERROR_LOG" >&2)
|
|
|
|
declare -A messages
|
|
|
|
connect() {
|
|
exec 3<>"/dev/tcp/$SERVER/$PORT"
|
|
if (($?)); then
|
|
return $?
|
|
fi
|
|
sendmsg NICK "$NICK"
|
|
if (($?)); then
|
|
return $?
|
|
fi
|
|
sendmsg USER "$LOGIN 8 *" "$REALNAME"
|
|
if (($?)); then
|
|
return $?
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
#:source COMMAND a r g s :message
|
|
# 2-src, 3-cmd, 5-arg, 7-msg
|
|
message_regex='^\(:\([^ ]\+\)\ \)\?\([A-Z0-9]\+\)\( \([^:]\+\)\)\?\( \?:\([^\r\n]*\)\)\?\([\r\n]*\)\?$'
|
|
|
|
parse_source() { #rawmsg
|
|
sed "s/$message_regex/\2/g" <<< "$1"
|
|
}
|
|
|
|
parse_command() { #rawmsg
|
|
sed "s/$message_regex/\3/g" <<< "$1"
|
|
}
|
|
|
|
parse_args() { #rawmsg
|
|
sed -e "s/$message_regex/\5/g" -e 's/^\s//' -e 's/\s$//' <<< "$1"
|
|
}
|
|
|
|
parse_message() { #rawmsg
|
|
sed "s/$message_regex/\7/g" <<< "$1"
|
|
}
|
|
|
|
#nick!login@host
|
|
# 1-nick, 2-login, 3-host
|
|
user_regex='^\([^!]\+\)!\([^@]\+\)@\(.*\)$'
|
|
|
|
parse_user_nick() { # rawsource
|
|
sed "s/$user_regex/\1/g" <<< "$1"
|
|
}
|
|
|
|
parse_user_login() { # rawsource
|
|
sed "s/$user_regex/\2/g" <<< "$1"
|
|
}
|
|
|
|
parse_user_host() { # rawsource
|
|
sed "s/$user_regex/\3/g" <<< "$1"
|
|
}
|
|
|
|
parse_ctcp() { # message
|
|
ctcp="$(sed 's/^\x01\([A-Za-z]\+\)\x01$/\U\1/g' <<< "$1")"
|
|
if [[ $ctcp != "$1" ]]; then
|
|
sed : <<< "$ctcp"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
parse_action() { # message
|
|
cmd="$(sed 's/^\x01\([A-Za-z]\+\) \(.\+\)\x01$/\U\1/g' <<< "$1")"
|
|
msg="$(sed 's/^\x01\([A-Za-z]\+\) \(.\+\)\x01$/\2/g' <<< "$1")"
|
|
if [[ $cmd == 'ACTION' && -n $msg ]]; then
|
|
sed : <<< "$msg"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
parse_targeted_nick() { # message
|
|
target="$(sed 's/^\([^ :,/]\+\)[:,]\? .\+/\1/' <<< "$1")"
|
|
if [[ -n $target && $target != "$1" ]]; then
|
|
sed : <<< "$target"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
parse_targeted_msg() { # message
|
|
target="$(sed 's/^[^ :,/]\+[:,]\? \(.\+\)/\1/' <<< "$1")"
|
|
if [[ -n $target && $target != "$1" ]]; then
|
|
sed : <<< "$target"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
trimrn() { # <string
|
|
sed -e 's/\r//g' -e 's/\n//g' -e 1q
|
|
}
|
|
|
|
indexof() { # haystack, needle
|
|
str="$1"
|
|
search="$2"
|
|
|
|
i=0;
|
|
while :; do
|
|
if ((i >= ${#str})); then
|
|
i=-1
|
|
break
|
|
fi
|
|
if [[ ${str:$i:1} == "$search" ]]; then
|
|
break
|
|
fi
|
|
((i++))
|
|
done
|
|
|
|
echo "$i"
|
|
}
|
|
|
|
regex() { # regex, text
|
|
msg="$1"
|
|
target="$2"
|
|
|
|
|
|
#tokenize
|
|
l=()
|
|
pos="$(indexof "$msg" '/')"
|
|
while ((pos >= 0)); do
|
|
i=0
|
|
((p=pos-i-1))
|
|
while ((p >= 0)) && [[ ${msg:$p:1} == '\' ]]; do # count \ characters
|
|
((i++))
|
|
((p=pos-i-1))
|
|
done
|
|
if ((i%2 == 0)); then
|
|
l+=("${msg:0:$pos}")
|
|
((p=pos+1))
|
|
msg="${msg:$p}"
|
|
pos=0
|
|
else
|
|
((pos++))
|
|
fi
|
|
p="$(indexof "${msg:$pos}" '/')"
|
|
if ((p >= 0)); then
|
|
((pos+=p))
|
|
else
|
|
pos=$p
|
|
fi
|
|
done
|
|
l+=("$msg")
|
|
|
|
# l is now an array of the expr separated by unescaped /
|
|
|
|
i=0
|
|
regexps=()
|
|
ok=1
|
|
|
|
# s/expr1/repl1/opts1 s/expr2/repl2/opts2 s/expr3/repl3
|
|
while ((i < ${#l[@]})); do
|
|
# begins with s
|
|
if [ "${l[$i]}" != "s" ]; then
|
|
break
|
|
fi
|
|
((i++))
|
|
|
|
# expr
|
|
if ((i >= ${#l[@]})); then
|
|
break
|
|
fi
|
|
exp="${l[$i]}"
|
|
((i++))
|
|
|
|
# repl
|
|
if ((i >= ${#l[@]})); then
|
|
break
|
|
fi
|
|
repl="${l[$i]}"
|
|
((i++))
|
|
|
|
# opts
|
|
opts=''
|
|
if ((i < ${#l[@]})); then
|
|
opts="${l[$i]}"
|
|
p=0
|
|
while ((p < ${#opts})); do
|
|
c="${opts:$p:1}"
|
|
if [[ $c != 'i' && $c != 'g' ]]; then
|
|
ok=0
|
|
break
|
|
fi
|
|
((p++))
|
|
done
|
|
if ! ((ok)); then
|
|
# multiple regexps per line
|
|
if [[ ${opts:$p:1} == ' ' ]]; then
|
|
p1=$p
|
|
while ((p < ${#opts})); do
|
|
if [[ ${opts:$p:1} != ' ' ]]; then
|
|
break
|
|
fi
|
|
p=$((p+1))
|
|
done
|
|
l[$i]="${opts:$p}"
|
|
opts="${opts:0:$p1}"
|
|
ok=1
|
|
else
|
|
break
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if ((ok)); then
|
|
regexps+=("s/$exp/$repl/$opts")
|
|
fi
|
|
done
|
|
|
|
|
|
if ! ((ok)) || ((${#regexps[@]} == 0)); then
|
|
return 1
|
|
fi
|
|
|
|
t="$target"
|
|
for re in "${regexps[@]}"; do
|
|
sed : <<< "$re" >&2
|
|
target="$(sed -e "$re" <<< "$target")"
|
|
done
|
|
target="$(trimrn <<< "$target")"
|
|
if [[ $target != "$t" ]]; then
|
|
if [[ -n $target ]]; then
|
|
trimrn <<< "$target"
|
|
return 0
|
|
fi
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
sendmsg() { # command, args, message
|
|
if (($# > 0)); then
|
|
cmd="$(sed -e 's/\(.*\)/\U\1/g' <<< "$1" | trimrn)"
|
|
if (($# > 1)); then
|
|
args="$(trimrn <<< "$2")"
|
|
if (($# > 2)); then
|
|
msg="$(trimrn <<< "$3")"
|
|
if [[ -n $args ]]; then
|
|
line="$cmd $args :$msg"
|
|
else
|
|
line="$cmd :$msg"
|
|
fi
|
|
else
|
|
line="$cmd $args"
|
|
fi
|
|
else
|
|
line="$cmd"
|
|
fi
|
|
fi
|
|
if [[ -n $line ]]; then
|
|
line="$(trimrn <<< "$line" | sed 's/^\(.\{,'"$MAX_LINE_LENGTH"'\}\).*/\1/')"
|
|
echo "$(date +%s.%N) >>> $line" >&4
|
|
sed : <<< "$line" >&3
|
|
if (($? == 0)); then
|
|
return 0
|
|
else
|
|
return $?
|
|
fi
|
|
else
|
|
echo 'invalid usage of message' >&2
|
|
exit 1
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
readmsg() {
|
|
IFS= read -r -u 3 -t "$READ_TIMEOUT" line
|
|
success=$?
|
|
echo "$(date +%s.%N) <<< $line" >&4
|
|
sed : <<< "$line"
|
|
return $success
|
|
}
|
|
|
|
readloop() {
|
|
while :; do
|
|
line="$(readmsg)"
|
|
if (($?)); then
|
|
echo 'disconnected from server' >&2
|
|
break
|
|
fi
|
|
cmd="$(parse_command "$line")"
|
|
|
|
case $cmd in
|
|
PING)
|
|
sendmsg PONG "$(parse_args "$line")" "$(parse_message "$line")"
|
|
;;
|
|
|
|
INVITE)
|
|
args="$(parse_args "$line")"
|
|
who="$(sed 's/\(\([^ ]*\)\( \|$\)\)\{1\}.*/\2/' <<< "$args")"
|
|
where="$(sed 's/\(\([^ ]*\)\( \|$\)\)\{2\}.*/\2/' <<< "$args")"
|
|
if [[ $where == "$who" || -z $where ]]; then
|
|
who="$args"
|
|
where="$(parse_message "$line")"
|
|
fi
|
|
if [[ $who == "$NICK" && -n $where && $ACCEPT_INVITES != 0 ]]; then
|
|
echo "invited to $where" >&2
|
|
sendmsg JOIN "$where"
|
|
CHANNELS+=("$where")
|
|
fi
|
|
;;
|
|
|
|
KICK)
|
|
args="$(parse_args "$line")"
|
|
where="$(sed 's/\(\([^ ]*\)\( \|$\)\)\{1\}.*/\2/' <<< "$args")"
|
|
who="$(sed 's/\(\([^ ]*\)\( \|$\)\)\{2\}.*/\2/' <<< "$args")"
|
|
if [[ $who == "$NICK" && -n $where ]]; then #TODO: got "too many arguments" error when others are kicked; is it fixed now?
|
|
echo "kicked from $where" >&2
|
|
i=0
|
|
while ((i < ${#CHANNELS})); do
|
|
if [[ ${CHANNELS[$i]} == "$where" ]]; then
|
|
unset -v "CHANNELS[$i]"
|
|
else
|
|
((i++))
|
|
fi
|
|
done
|
|
fi
|
|
;;
|
|
|
|
433)
|
|
echo "nick $NICK is already taken" >&2
|
|
exit 1
|
|
;;
|
|
|
|
PRIVMSG)
|
|
where="$(parse_args "$line")"
|
|
user="$(parse_source "$line")"
|
|
nick="$(parse_user_nick "$user")"
|
|
msg="$(parse_message "$line")"
|
|
if [[ $where == "$NICK" ]]; then
|
|
where="$nick"
|
|
ctcp="$(parse_ctcp "$msg")"
|
|
if (($? == 0)); then
|
|
if [[ $ctcp == VERSION ]]; then
|
|
version="$(sed --version 2>&1 | trimrn)" # some seds don't have --version, have to fix?
|
|
sendmsg NOTICE "$where" "$(sed 's/^.*$/\x01&\x01/' <<< "$ctcp $version")"
|
|
continue
|
|
fi
|
|
fi
|
|
fi
|
|
targetednick="$(parse_targeted_nick "$msg")"
|
|
istargeted=$?
|
|
targetedkey="$where $targetednick"
|
|
origmsg="$msg"
|
|
origkey="$where $nick"
|
|
if [[ $istargeted == 0 && -n "${messages[$targetedkey]}" ]]; then
|
|
nick="$targetednick"
|
|
msg="$(parse_targeted_msg "$msg")"
|
|
fi
|
|
key="$where $nick"
|
|
target="${messages[$key]}"
|
|
action="$(parse_action "$target")"
|
|
if (($? == 0)); then
|
|
target="$action"
|
|
fromnick="$(printf "\\x02* %s\\x02" "$nick")"
|
|
else
|
|
fromnick="<$nick>"
|
|
fi
|
|
regexed="$(regex "$msg" "$target")"
|
|
if (($? == 0)); then
|
|
sendmsg PRIVMSG "$where" "$fromnick $regexed"
|
|
elif [[ $istargeted != 0 ]]; then
|
|
messages[$key]="$msg"
|
|
else
|
|
messages[$origkey]="$origmsg"
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
main() {
|
|
while :; do
|
|
if connect; then
|
|
sleep "$SLEEP_JOIN"
|
|
for ch in "${CHANNELS[@]}"; do
|
|
sendmsg JOIN "$ch"
|
|
done
|
|
readloop
|
|
fi
|
|
echo 'reconnecting in 10 seconds...' >&2
|
|
sleep "$SLEEP_RECONNECT"
|
|
done
|
|
}
|
|
|
|
main
|