rowbot/rowbot

345 lines
5.9 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 lastpipe 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 (( $# != 2 )); 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}
else
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 ))
}
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
# irc registration settings
get_option nick rowbot-dev
get_option ident rowbot
get_option realname rowbot
get_option chan ""
# bot control settings
get_option owner "${USER:-uplime}"
get_option trigger \`
get_option dev yes
###
# bootup sequence
###
on_first_001_bootup() {
log_info "rowbot's pid is %d" "$$"
}
###
# 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_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_exit_zzz_log() {
if [[ -v log_fd ]] && (( log_fd != 1 )); then
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_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_first_002_net() {
local irc_sock 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-out.sock {in_sock}<"$sock_dir"/rowbot-in.sock
log_debug "process %d is handling tls" "$tls_pid"
fi
}
###
# cleanup
###
cleanup() {
log_info "Theres a lot of beauty in ordinary things. Isnt that kind of the point?"
run_callbacks on_exit_
}
trap cleanup EXIT
###
# live code reloader
###
reload_config() {
local setting setting_name
run_callbacks on_before_
for setting in "${!config[@]}"; do
setting_name=${setting//-/_}
export "${setting_name^^}"="${config[$setting]}"
done
exec "${cmd_line[@]}"
}
reload_hup() {
log_info "received reload signal (HUP)"
reload_config
}
trap reload_hup HUP
###
# initialization sequence
###
run_callbacks on_init_
if is_reloaded; then
run_callbacks on_after_
else
run_callbacks on_first_
fi