#!/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 "$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_001_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 [[ -v 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 "There’s a lot of beauty in ordinary things. Isn’t 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