rowbot/rowbot

588 lines
11 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
###
# feature switch toggling
###
shopt -s dotglob extglob nullglob
###
# utility and helper functions
###
any_file() {
local files=( "${1:-.}"/* ) max idx
if (( ${#files[@]} > 1 )); then
(( max = ${#files[@]} - 1 ))
idx=$(random 0 "$max")
printf %s "${files[$idx]}"
fi
}
die() {
local fmt=$1
shift
# This is a wrapper around printf so the format string isn't known ahead of
# time.
# shellcheck disable=SC2059
printf "FATAL: $fmt\n" "$@" >&2
exit "${STATUS:-42}"
}
get_option() {
if (( ! $# )); then
return 1
fi
local var_name=${1//-/_}
local env_var=${var_name^^}
if [[ -v $env_var ]]; then
config[$1]=${!env_var}
elif [[ -v opts[$1] ]]; then
config[$1]=${opts[$1]}
elif [[ -v $var_name ]]; then
config[$1]=${!var_name}
elif (( $# > 1 )); then
config[$1]=$2
fi
}
has() {
if (( $# )); then
hash "$1" 2>/dev/null
else
return 1
fi
}
is_parent() {
(( BASHPID == $$ ))
}
is_reloaded() {
[[ $RELOADED = yes ]] || (( LORE_LIVES > 1 ))
}
is_running () {
kill -0 "$1" 2>/dev/null
}
random() {
local min=$1 max=$2
(( (RANDOM % max) + min ))
}
run_callbacks() {
if (( ! $# )); then
return 1
fi
local filter=$1
shift
while IFS= read -r; do
"$REPLY" "$@"
done < <(compgen -A function "$filter")
return 0
}
###
# configure rowbot's environment
###
# parse command line arguments
declare -A opts
cmd_line=( "${@:0}" )
while (( $# )); do
case $1 in
--*=*)
key=${1#--} key=${key%%=*}
opts[$key]=${1#--*=}
;;
--no-*)
key=${1#--no-}
opts[$key]=no
;;
--)
shift
break
;;
--*)
key=${1#--}
opts[$key]=yes
;;
*)
break
esac
shift
done
# load custom configuration files
for file do
if [[ -f $file ]]; then
# These files (if any) are provided dynamically at run-time.
# shellcheck disable=SC1090
. "$file" # ha, dot file
else
die "could not locate config file %s" "$file"
fi
done
# cleanup
unset key file
###
# load default config
###
declare -A config
get_option owner "${USER:-uplime}"
get_option trigger \`
get_option dev yes
###
# bootup/shutdown sequence
###
on_sys_first_001_bootup() {
log_info "rowbot's pid is %d" "$$"
get_option nick rowbot-dev
get_option ident rowbot
get_option realname rowbot
}
on_sys_first_999_bootup() {
log_debug "registering with the server"
irc_nick "${config[nick]}"
irc_user "${config[ident]}" "${config[realname]}"
}
on_sys_before_999_bootup() {
local setting setting_name
log_debug "storing the config"
for setting in "${!config[@]}"; do
setting_name=${setting//-/_}
export "CONFIG_${setting_name^^}=${config[$setting]}"
done
}
on_sys_after_001_bootup() {
log_debug "reloading the config"
local setting setting_name
while IFS= read -r setting; do
setting_name=${setting#CONFIG_} setting_name=${setting_name//_/-}
config[${setting_name,,}]=${!setting}
unset "$setting"
done < <(compgen -e CONFIG_)
}
on_sys_exit_999_bootup() {
log_info "Theres a lot of beauty in ordinary things. Isnt that kind of the point?"
}
###
# logger
###
log() {
if [[ -v LOG_LEVEL ]] && (( log_levels[$log_level] <= log_levels[$LOG_LEVEL] )); then
printf "%s: $1\n" "${LOG_LEVEL^^}" "${@:2}" >&"$log_fd"
fi
}
log_debug() {
LOG_LEVEL=debug log "$@"
}
log_info() {
LOG_LEVEL=info log "$@"
}
log_warn() {
LOG_LEVEL=warn log "$@"
}
log_error() {
LOG_LEVEL=error log "$@"
}
log_has_level() {
local level
for level in "${!log_levels[@]}"; do
if [[ ${1,,} = "$level" ]]; then
return 0
fi
done
return 1
}
on_sys_init_001_log() {
declare -gA log_levels=( [debug]=1 [info]=2 [warn]=3 [error]=4 )
get_option log-level info
if ! log_has_level "${config[log-level]}"; then
die "%s is not a valid logging level" "${config[log-level]}"
fi
get_option log ""
get_option overwrite no
log_level=${config[log-level]}
if [[ ${config[log]} ]]; then
if [[ ${config[overwrite]} = yes ]]; then
exec {log_fd}>"${config[log]}"
else
exec {log_fd}>>"${config[log]}"
fi
else
log_fd=1
fi
}
on_sys_before_999_log() {
if [[ -v log_fd ]] && (( log_fd != 1 )); then
log_debug "shutting logger down for reload"
exec {log_fd}>&-
fi
}
on_sys_exit_999_log() {
if [[ -v log_fd ]] && (( log_fd != 1 )); then
log_debug "shutting logger down for good"
exec {log_fd}>&-
fi
}
###
# net code
###
net_recv() {
declare -n sock_line=$1
IFS= read -r "$1" <&"$in_sock"
sock_line=${sock_line%$'\r'}
log_debug "received line: %s" "$sock_line"
}
net_send() {
local fmt
# As this is a printf wrapper, the format string is provided as an argument.
# shellcheck disable=SC2059
printf -v fmt "$1" "${@:2}"
printf '%s\r\n' "$fmt" >&"$out_sock"
log_debug "sending line: %s" "$fmt"
}
on_sys_init_002_net() {
get_option server irc.libera.chat
get_option tls no
if [[ ${config[tls]} = no ]]; then
get_option port 6667
else
get_option client-cert ""
get_option port 6697
fi
}
on_sys_first_002_net() {
local conn_args
if [[ ${config[tls]} = no ]]; then
log_info "rowbot is connecting to irc://%s:%s" "${config[server]}" "${config[port]}"
exec {irc_sock}<>/dev/tcp/"${config[server]}"/"${config[port]}"
in_sock=$irc_sock out_sock=$irc_sock
else
if ! has socat; then
die "please install socat to use tls with rowbot."
fi
log_info "rowbot is connecting to ircs://%s:%s" "${config[server]}" "${config[port]}"
sock_dir=$(mktemp -d)
log_debug "socket directory is %s" "$sock_dir"
mkfifo "$sock_dir"/rowbot-{in,out}.sock
# This is a false positive
# shellcheck disable=SC2102
if [[ ${config[client-cert]} ]]; then
if [[ ! -f ${config[client-cert]} ]]; then
die "client certificate not found: %s" "${config[client-cert]}"
fi
conn_args=OPENSSL:${config[server]}:${config[port]},cert=${config[client-cert]}
else
conn_args=OPENSSL:${config[server]}:${config[port]}
fi
socat "$conn_args" - <"$sock_dir"/rowbot-in.sock >"$sock_dir"/rowbot-out.sock &
tls_pid=$!
exec {out_sock}>"$sock_dir"/rowbot-in.sock {in_sock}<"$sock_dir"/rowbot-out.sock
log_debug "process %d is handling tls" "$tls_pid"
fi
}
on_sys_before_002_net() {
if [[ ${config[tls]} = no ]]; then
export IRC_SOCK=$irc_sock
else
export SOCK_DIR=$sock_dir TLS_PID=$tls_pid
export OUT_SOCK=$out_sock IN_SOCK=$in_sock
fi
}
on_sys_after_002_net() {
if [[ ${config[tls]} = no ]]; then
irc_sock=$IRC_SOCK out_sock=$irc_sock in_sock=$irc_sock
unset IRC_SOCK
else
sock_dir=$SOCK_DIR tls_pid=$TLS_PID out_sock=$OUT_SOCK in_sock=$IN_SOCK
unset SOCK_DIR TLS_PID OUT_SOCK IN_SOCK
fi
}
on_sys_exit_998_net() {
if [[ ${config[tls]} = no ]]; then
log_info "rowbot is closing the connection to irc://%s:%s" "${config[server]}" "${config[port]}"
exec {irc_sock}>&-
else
log_info "rowbot is closing the connection to ircs://%s:%s" "${config[server]}" "${config[port]}"
if [[ -v tls_pid ]]; then
kill -INT "$tls_pid"
fi
rm -rf -- "$sock_dir"
fi
}
###
# irc magic
###
magic_annoyatron900() {
irc_ping "row your bot gently down the stream"
}
on_sys_init_999_magic() {
get_option chan ""
}
on_sys_before_999_magic() {
if [[ -v alarm_pid ]]; then
export ALARM_PID=$alarm_pid
fi
}
on_sys_after_999_magic() {
trap magic_annoyatron900 USR1
if [[ -v ALARM_PID ]]; then
alarm_pid=$ALARM_PID
fi
}
on_sys_register_999_magic() {
if [[ ${config[chan]} ]]; then
irc_join "$chan"
fi
while true; do
read -rt 10 </dev/zero
KILL -USR1 "$$"
done &
alarm_pid=$!
trap magic_annoyatron900 USR1
log_debug "process %d is being annoying" "$alarm_pid"
}
on_sys_exit_997_magic() {
log_debug "shutting down annoyatron900"
if [[ -v alarm_pid ]] && is_running "$alarm_pid"; then
kill -STOP "$alarm_pid"
fi
}
# nick=${args[0]}
# who "$nick" %%uht,42
###
# irc receive handlers
###
irc_on_NOTICE() {
log_info "[%s/%s] %s" "${msg[from]}" "${msg_args[0]}" "${msg_args[1]}"
}
irc_on_PING() {
irc_pong "${msg_args[1]}"
log_debug "received ping: %s" "${msg_args[0]}"
}
irc_on_PONG() {
log_debug "received pong: %s" "${msg_args[1]}"
}
irc_on_001() {
log_info %s "${msg_args[1]}"
run_callbacks on_sys_register_
run_callbacks on_register_
}
###
# irc send handlers
###
irc_join() {
local chans
printf -v chans %s, "$@"
net_send "JOIN %s" "${chans%,}"
}
irc_nick() {
net_send "NICK :%s" "$1"
}
irc_ping() {
net_send "PING :%s" "$1"
}
irc_user() {
net_send "USER %s 0 * :%s" "$1" "$2"
}
###
# cleanup
###
cleanup() {
run_callbacks on_exit_
run_callbacks on_sys_exit_
}
trap cleanup EXIT
###
# live code reloader
###
reload_config() {
run_callbacks on_sys_before_
run_callbacks on_before_
RELOADED=yes exec "${cmd_line[@]}"
}
reload_hup() {
log_info "received reload signal (HUP)"
reload_config
}
trap reload_hup HUP
###
# initialization sequence
###
run_callbacks on_sys_init_
run_callbacks on_init_
if is_reloaded; then
run_callbacks on_sys_after_
run_callbacks on_after_
else
run_callbacks on_sys_first_
run_callbacks on_first_
fi
###
# driver/protocol parser
###
while net_recv line; do
declare -A msg=( [words]=no [original]="$line" [score]=0 )
# parse prefix in the style of nick!ident@host
if [[ ${line:0:1} = :* ]]; then
prefix=${line%% *} prefix=${prefix#:} line=${line#:"$prefix"} line=${line# }
log_debug "parsing message prefix %s" "$prefix"
msg[host]=${prefix#*@} prefix=${prefix%"${msg[host]}"} prefix=${prefix%@}
msg[from]=${msg[host]}
if [[ $prefix ]]; then
msg[ident]=${prefix#*!}
msg[from]=${msg[ident]}
if [[ ${msg[ident]} != "$prefix" ]]; then
msg[nick]=${prefix%!*}
msg[from]=${msg[nick]}
fi
fi
if [[ /${msg[host]}/ = */bot/* ]]; then
(( msg[score] += 100 ))
fi
if [[ ${msg[nick]} = *-bot ]]; then
(( msg[score] += 30 ))
elif [[ ${msg[nick]} = *bot ]]; then
(( msg[score] += 15 ))
fi
fi
# parse command formatted as a 3 digit integer or as a word consisting of
# alphabet values
msg[cmd]=${line%% *} line=${line#"${msg[cmd]}"} line=${line# }
log_debug "parsing message command %s" "${msg[cmd]}"
# parse the remaining values into white-space separated arguments
msg_args=()
while [[ $line ]]; do
if [[ ${line:0:1} = : ]]; then
msg_args+=( "${line:1}" ) msg[words]=yes line=
# Code using this array will be implemented later.
# shellcheck disable=SC2034
read -ra msg_words <<< "${msg_args[-1]}"
log_debug "parsed final argument %s" "${msg_args[-1]}"
else
arg=${line%% *} msg_args+=( "$arg" ) line=${line#"$arg"} line=${line# }
log_debug "parsed argument %s" "$arg"
fi
done
if [[ ${msg[cmd]^^} = @(PRIVMSG|NOTICE) ]]; then
case ${msg_args[-1]} in
"["*"]")
(( msg[score] += 20 ))
;;
$'\xe2\x80\x8b'*)
(( msg[score] += 100 ))
esac
fi
log_debug "bot score is %d" "${msg[score]}"
if has irc_on_"${msg[cmd]^^}"; then
irc_on_"${msg[cmd]^^}"
else
log_warn "unhandled line: %s" "${msg[original]}"
fi
done