#!/usr/bin/env bash ### # lore ### #RELEASE_CODE=Bender #RELEASE_VER=1.0 #RELEASE_NAME="Bender (Rowbot v1.0)" #RELEASE_DESC="A smol bot for remembering things" printf -v LORE_THIS_RELOAD '%(%s)T' -1 if [[ ! -v LORE_START_TIME ]]; then export LORE_START_TIME=$LORE_THIS_RELOAD fi if [[ ! -v LORE_LONGEST_LIFE ]]; then export LORE_LONGEST_LIFE=0 fi if [[ -v LORE_LIVES ]]; then (( LORE_LIVES += 1 )) else export LORE_LIVES=1 fi ### # switch toggler ### shopt -s nullglob dotglob extglob ### # logger ### declare -A levels=( [debug]=1 [info]=2 [warn]=3 [error]=4 ) log() { if [[ -v LEVEL ]] && (( levels[$level] <= levels[$LEVEL] )); then printf "%s: $1\n" "${LEVEL^^}" "${@:2}" >&"$log_fd" fi } debug() { LEVEL=debug log "$@" } info() { LEVEL=info log "$@" } warn() { LEVEL=warn log "$@" } error() { LEVEL=error log "$@" } ### # utilities ### has() { hash "$1" 2>/dev/null } die() { local status=1 if (( $# > 1 )) && [[ $1 = -s ]]; then status=$2 shift shift fi error "$@" exit "$status" } is-parent() { (( BASHPID == $$ )) } is-channel() { [[ ${1:0:1} = \# ]] } is-bot() { local score=0 if [[ ${args[-1]:0:3} = $'\xe2\x80\x8b' ]]; then (( score += 100 )) fi if [[ $from = *-bot ]]; then (( score += 30 )) elif [[ $from = *bot ]]; then (( score += 15 )) fi if [[ /$host/ = */bot/* ]]; then (( score += 100 )) fi if [[ ${args[-1]} = '['*']' ]]; then (( score += 20 )) fi printf %d "$score" } is-log-level() { local level for level in "${!levels[@]}"; do if [[ ${1,,} = "$level" ]]; then return 0 fi done return 1 } seconds() { local days hours minutes seconds time (( days = $1 / 60 / 60 / 24 )) (( hours = $1 / 60 / 60 % 24 )) (( minutes = $1 / 60 % 60 )) (( seconds = $1 % 60 )) if (( days )); then if (( days == 1 )); then time="1 day" else time="$days day" fi fi if (( hours )); then if [[ $time ]]; then time+=", " fi time+="$hours hour" if (( hours > 1 )); then time+=s fi fi if (( minutes )); then if [[ $time ]]; then time+=", " fi time+="$minutes minute" if (( minutes > 1 )); then time+=s fi fi if [[ $time ]]; then time+=" and " fi time+="$seconds second" if (( seconds > 1 )); then time+=s fi printf -- %s "$time" } ### # argument parser for parsing arguments ### original_args=( "$@" ) declare -A opts 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 prog_args=( "$@" ) ### # default config ### server=irc.libera.chat port=6667 tls=no client_cert= nick=rowbot-dev ident=rowbot realname=rowbot chan= trigger=\` fact_root=. reload=no level=info log_fd=1 log= sys_root=sysfacts owner=${USER:-uplime} dev=no markov_seed= overwrite=no ### # apply custom config ### for file do if [[ -f $file ]]; then # These files are provided dynamically at run-time. # shellcheck disable=SC1090 . "$file" # ha, dot file else die "could not locate config file %s" "$file" fi done ### # apply command line options ### if [[ -v opts[tls] ]]; then tls=${opts[tls]} fi # This is a false positive. # shellcheck disable=SC2102 if [[ -v opts[level] ]]; then if is-log-level "${opts[level]}"; then level=${opts[level],,} else die "%s is not a valid logging level" "${opts[level]}" fi fi if [[ $tls = yes ]]; then # This is a false positive. # shellcheck disable=SC2102 if ! has socat; then die "please install socat to use tls with rowbot." elif [[ -v opts[client-cert] ]]; then client_cert=${opts[client-cert]} fi port=6697 fi config=( server port nick ident realname chan trigger fact_root reload dev log owner sys_root reload markov_seed ) for opt in "${config[@]}"; do declare -n config_var=$opt cmd_arg=${opt//_/-} if [[ -v opts[$cmd_arg] ]]; then # This variable is only used for assignment. # shellcheck disable=SC2034 config_var=${opts[$cmd_arg]} fi done if [[ $log && $reload = no ]]; then if [[ $overwrite = yes ]]; then exec {log_fd}>"$log" else exec {log_fd}>>"$log" fi fi ### # apply reloaded values ### if [[ $markov_seed ]]; then if [[ -f $markov_seed ]]; then debug "loading markov seed file" declare -A markov_chains enabled read -ra words < "$markov_seed" for (( idx = 0; idx < ${#words[@]} - 2; idx++ )); do key="${words[$idx]} ${words[$idx + 1]}" if [[ -z ${markov_chains[$key]} ]]; then markov_chains[$key]=${words[$idx + 2]} else markov_chains[$key]+=" ${words[$idx + 2]}" fi done else die "seed file does not exist: %s" "$markov_seed" fi fi if [[ $reload = yes ]]; then if [[ -v RECORDING ]]; then IFS=, read -ra recording <<< "$RECORDING" for pair in "${recording[@]}"; do enabled[${pair%=*}]=${pair##*=} done fi reload_vars=( nick ident host level log log_fd alarm_pid tls_pid in_sock out_sock sock_dir sys_root fact_root dev trigger registered keep_trying desired_nick overwrite ) for var in "${reload_vars[@]}"; do env_var=${var^^} declare -n reload_var=$var if [[ -v $env_var ]]; then # This variable is only used for assignment. # shellcheck disable=SC2034 reload_var=${!env_var} fi done fi ### # process/resource management ### cleanup() { debug "reaping rowbot's children" if [[ -v alarm_pid ]]; then debug "killing alarm process (%d) from %d" "$alarm_pid" "$BASHPID" kill "$alarm_pid" fi if [[ -v tls_pid ]]; then debug "killing tls process (%d) from %d" "$tls_pid" "$BASHPID" kill "$tls_pid" rm -rf "$sock_dir" fi if [[ -v tls_pid || $tls = no ]]; then exec {in_sock}>&- {out_sock}>&- if (( log_fd != 1 )); then exec {log_fd}>&- fi fi } if is-parent; then trap cleanup EXIT fi alarm-handler() { ping "row your bot gently down the stream" if [[ $keep_trying = yes ]]; then info "trying for %s again" "$desired_nick" nick "$desired_nick" fi } trap alarm-handler ALRM config-reload() { local file key recording info "received HUP signal" for file in "${prog_args[@]}"; do if [[ -f $file ]]; then debug "loading config file %s" "$file" # These files are provided dynamically at run-time. # shellcheck disable=SC1090 . "$file" else die "could not locate config file %s" "$file" fi done if (( log_fd != 1 )); then debug "closing and re-opening log" exec {log_fd}>&- if [[ $overwrite = yes ]]; then exec {log_fd}>"$log" else exec {log_fd}>>"$log" fi fi if [[ -v markov_chains[@] ]]; then for key in "${!enabled[@]}"; do # This is a scalar string, not an array. # shellcheck disable=SC2179 recording+=",$key=${enabled[$key]}" done export RECORDING=${recording#,} fi reload_vars=( nick ident host level log log_fd alarm_pid tls_pid in_sock out_sock sock_dir sys_root fact_root dev trigger registered keep_trying desired_nick overwrite ) for env_var in "${reload_vars[@]}"; do export "${env_var^^}"="${!env_var}" done if (( SECONDS > LORE_LONGEST_LIFE )); then export LORE_LONGEST_LIFE=$SECONDS fi info "reloading rowbot" exec "$0" --reload "${original_args[@]}" } trap config-reload HUP ### # net code ### if [[ $reload = no && $tls = yes ]]; then sock_dir=$(mktemp -d) mkfifo "$sock_dir"/rb{in,out} if [[ $client_cert ]]; then if [[ ! -f $client_cert ]]; then die "client certificate not found: %s" "$client_cert" fi conn_args=OPENSSL:$server:$port,cert=$client_cert else conn_args=OPENSSL:$server:$port fi socat "$conn_args" - <"$sock_dir"/rbin >"$sock_dir"/rbout & tls_pid=$! exec {out_sock}>"$sock_dir"/rbin {in_sock}<"$sock_dir"/rbout debug "created tls connection (pid %d)" "$tls_pid" elif [[ $reload = no ]]; then exec {sock}<>/dev/tcp/"$server"/"$port" in_sock=$sock out_sock=$sock debug "created plaintext connection" fi 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" debug "sending line: %s" "$fmt" } recv() { declare -n sock_line=$1 IFS= read -r "$1" <&"$in_sock" sock_line=${sock_line%$'\r'} debug "received line: %s" "$sock_line" } ### # irc recv code ### on_ERROR() { error "${args[0]}" exit } on_JOIN() { info "%s has joined %s" "$from" "${args[0]}" } on_KICK() { if (( ${#args[@]} == 3 )); then info "%s has kicked %s from %s: %s" "$from" "${args[1]}" "${args[0]}" "${args[-1]}" else info "%s has kicked %s from %s" "$from" "${args[1]}" "${args[0]}" fi } on_MODE() { if (( ${#args[@]} == 2 )); then info "%s sets mode(s) %s on %s" "$from" "${args[1]}" "${args[0]}" elif (( ${#args[@]} > 2 )); then info "%s: %s sets mode(s) %s" "${args[0]}" "$from" "${args[*]:1}" fi } on_NICK() { if [[ $from = "$nick" ]]; then nick=${args[0]} fi info "%s has changed their name to %s" "$from" "${args[0]}" } on_NOTICE() { info "[%s/%s] %s" "$from" "${args[0]}" "${args[1]}" } on_PART() { if (( ${#args[@]} > 1 )); then info "%s has left %s: %s" "$from" "${args[0]}" "${args[1]}" else info "%s has left %s" "$from" "${args[0]}" fi } on_PING() { pong "${args[1]}" debug "received ping: %s" "${args[0]}" } on_PONG() { debug "received pong: %s" "${args[1]}" } on_PRIVMSG() { info "<%s/%s> %s" "$from" "${args[0]}" "${args[1]}" } on_TOPIC() { info "%s has changed the topic for %s: %s" "$from" "${args[0]}" "${args[1]}" } on_QUIT() { info "%s has disconnected: %s" "$from" "${args[0]}" } on_001() { info %s "${args[1]}" if [[ $chan ]]; then join "$chan" fi { debug "timer pid is %d" "$BASHPID" while true; do kill -ALRM "$$" sleep 10 done } & alarm_pid=$! nick=${args[0]} registered=yes who "$nick" %%uht,42 } on_002() { info %s "${args[1]}" } on_003() { info %s "${args[1]}" } on_004() { debug "%s " "${args[@]:1}" } declare -A isupport on_005() { local param key value for param in "${args[@]:1:${#args[@]}-2}"; do # This is a valid assignment, not a comparison. # shellcheck disable=SC1097 IFS== read -r key value <<< "$param" # While isupport is unused, it's still there in case later code wants to # use it. # shellcheck disable=SC2034 isupport[$key]=$value debug "isupport: %s = %s" "$key" "$value" done } on_250() { info %s "${args[1]}" } on_251() { info %s "${args[1]}" } on_252() { info "There are %d operators online" "${args[1]}" } on_253() { info "There are %d unknown connections" "${args[1]}" } on_254() { info "There are %d channels formed" "${args[1]}" } on_255() { info %s "${args[1]}" } on_265() { info %s "${args[3]}" } on_266() { info %s "${args[3]}" } on_315() { debug "end of WHO for %s" "${args[1]}" } on_332() { info "topic for %s is %s" "${args[1]}" "${args[2]}" } on_333() { local date printf -v date '%(%c)T' "${args[3]}" info "topic for %s set by %s at %s" "${args[1]}" "${args[2]}" "$date" } on_353() { info "members of %s: %s" "${args[2]}" "${args[3]}" } on_354() { if (( args[1] == 42 )); then debug "received the identifying who" ident=${args[2]} host=${args[3]} debug "ident=%s host=%s" "$ident" "$host" fi } on_366() { debug "%s: end of NAMES list" "${args[1]}" } on_372() { info %s "${args[1]}" } on_375() { debug %s "${args[1]}" } on_376() { debug %s "${args[1]}" } on_433() { info "somebody is already using %s" "${args[1]}" if [[ $registered = no ]]; then nick "${nick}_" fi } on_438() { error "${args[1]} couldn't change their nick to ${args[2]}: ${args[-1]}" } on_473() { error "%s: %s" "${args[1]}" "${args[2]}" } ### # irc send code ### join() { send "JOIN %s" "$1" } nick() { send "NICK %s" "$1" } notice() { if [[ -v host ]]; then local msg_len msg=$2 (( msg_len = 494 - (${#nick} + ${#ident} + ${#host} + ${#1}) )) debug "max message length is %d" "$msg_len" while (( ${#msg} > msg_len )); do send "NOTICE %s :"$'\xe2\x80\x8b'"%s" "$1" "${msg:0:$msg_len}" info "[%s/%s] %s" "$nick" "$1" "${msg:0:$msg_len}" msg=${msg:$msg_len} done fi send "NOTICE %s :%s" "$1" "$2" info "[%s/%s] %s" "$nick" "$1" "$2" } part() { if (( $# )); then if (( $# > 1 )); then send "PART $1 :$2" else send "PART $1" fi fi } ping() { send "PING :%s" "$1" } pong() { send "PONG %s" "$1" } privmsg() { if [[ -v host ]]; then local msg_len msg=$2 (( msg_len = 493 - (${#nick} + ${#ident} + ${#host} + ${#1}) )) debug "max message length is %d" "$msg_len" while (( ${#msg} > msg_len )); do send "PRIVMSG %s :"$'\xe2\x80\x8b'"%s" "$1" "${msg:0:$msg_len}" info "<%s/%s> %s" "$nick" "$1" "${msg:0:$msg_len}" msg=${msg:$msg_len} done fi send "PRIVMSG %s :"$'\xe2\x80\x8b'"%s" "$1" "$msg" info "<%s/%s> %s" "$nick" "$1" "$msg" } quit() { if (( $# )); then send "QUIT :%s" "$1" else send QUIT fi } user() { send "USER %s 0 * :%s" "$ident" "$realname" } who() { if (( $# > 1 )); then send "WHO %s %s" "$1" "$2" else send "WHO %s" "$1" fi } ### # app hooks ### hook_pre_PRIVMSG_nolog() { local words read -ra words <<< "${args[-1]}" if [[ ${words[0]} = "[nolog]" || ${words[0]} = nolog: ]]; then info "this message was redacted" return 1 else return 0 fi } hook_pre_PRIVMSG_CTCP() { if [[ ${args[1]} != $'\x01'*$'\x01' ]]; then return 0 fi local cmd msg cmd=${args[1]#$'\x01'} cmd=${cmd%% *} msg=${args[1]#* } msg=${msg%$'\x01'} if [[ ${cmd^^} = ACTION ]]; then if [[ ${args[0]:0:1} = \# ]]; then info "ctcp: %s: %s %s" "${args[0]}" "$from" "$msg" else info "privately, %s %s" "$from" "$msg" fi return 1 elif [[ ${args[0]:0:1} = \# ]]; then info "ctcp: %s has requested %s in %s" "$from" "${cmd^^}" "${args[0]}" return 1 fi info "ctcp: sending %s to %s" "${cmd^^}" "$from" case ${cmd^^} in CLIENTINFO) notice "$from" $'\x01'"CLIENTINFO ACTION CLIENTINFO PING SOURCE TIME VERSION"$'\x01' ;; PING) local msg msg=${args[1]#* } msg=${msg%$'\x01'} notice "$from" $'\x01'"PING $msg"$'\x01' ;; SOURCE) notice "$from" $'\x01'"SOURCE https://github.com/uplime/rowbot"$'\x01' ;; TIME) notice "$from" $'\x01'"TIME time for you to get a watch"$'\x01' ;; VERSION) notice "$from" $'\x01'"VERSION rowbot v2"$'\x01' esac return 1 } hook_post_PRIVMSG_markov() { if [[ ! -v markov_chains[@] ]]; then return 0 elif [[ ${to:0:1} = \# && ${enabled[$to]} != yes ]]; then return 0 elif (( ${#action_args[@]} < 3 )); then return 0 fi local idx key for (( idx = 0; idx < ${#action_args[@]} - 2; idx++ )); do key="${action_args[$idx]} ${action_args[$idx + 1]}" if [[ -z ${markov_chains[$key]} ]]; then markov_chains[$key]=${words[$idx + 2]} else markov_chains[$key]+=" ${words[$idx + 2]}" fi done } hook_cmd_markov() { local keys idx key str chains next vals val_idx if [[ ! -v markov_chains[@] ]]; then if [[ $action = markov ]]; then privmsg "$to" "markov chains are not enabled" fi return 0 fi case $action in enable) if (( ${#action_args[@]} )); then enabled[${action_args[0]}]=yes privmsg "$to" "now recording ${action_args[0]}" else enabled[$to]=yes privmsg "$to" "now recording $to" fi ;; disable) if (( ${#action_args[@]} )); then enabled[${action_args[0]}]=no privmsg "$to" "no longer recording ${action_args[0]}" else enabled[$to]=yes privmsg "$to" "no longer recording $to" fi ;; enabled\?) if [[ ${to:0:1} != \# || ${enabled[$to]} = yes ]]; then privmsg "$to" "yes, I am recording $to for markov chains." else privmsg "$to" "no, I am not recording $to for markov chains" fi ;; markov) keys=("${!markov_chains[@]}") (( idx = RANDOM % ${#keys[@]} )) key=${keys[$idx]} str=$key chains=0 while [[ -v markov_chains[$key] ]] && (( chains++ < 100 )); do read -ra vals <<< "${markov_chains[$key]}" (( val_idx = RANDOM % ${#vals[@]} )) next="$key ${vals[$val_idx]}" next=${next#* } str+=" ${next#* }" key=$next done privmsg "$to" "$from: $str" esac } hook_cmd_factoids() { local key val facts msg case $action in is) key=${action_line%% *} if [[ $key = "$action_line" ]]; then privmsg "$to" "$from: no fact provided" return 0 fi val=${action_line#"$key" } privmsg "$to" "I'm sure I'll remember that." mkdir -p "$fact_root"/"$to" printf %s "$val" > "$fact_root"/"$to"/"$key" ;; isnt) if [[ -f $fact_root/$to/$action_line ]]; then privmsg "$to" "I forgot what that was anyways." rm -f "$fact_root"/"$to"/"$action_line" fi ;; ls) facts=( "$fact_root"/"$to"/* ) privmsg "$to" "${facts[*]##*/}" ;; *) if [[ -f $fact_root/$to/$action ]]; then msg=$(<"$fact_root"/"$to"/"$action") if [[ ${action_args[0]} = \> ]] && (( ${#action_args[@]} > 1 )); then privmsg "$to" "${action_args[-1]}: $msg" else privmsg "$to" "$from: $msg" fi fi esac } hook_cmd_sysroot() { if [[ -d $sys_root && $action = sysfact ]]; then local files=( "$sys_root"/* ) idx msg if (( ${#files[@]} )); then (( idx = RANDOM % ${#files[@]} )) msg=$(<"${files[$idx]}") privmsg "$to" "sysfact #$(( idx + 1 )): $msg" fi fi } hook_cmd_control_panel() { if [[ $from != "$owner" && $dev != yes ]]; then if [[ $action = @(raw|join|reload|level|dev|dev\?|trigger|cycle|dev) ]]; then privmsg "$to" "$from is not in the sudoers file. This incident will be reported." info "security breach from %s in sector %s" "$from" "$to" fi return 0 fi local channel key recording env_var reload_vars recipient msg case $action in raw) info "%s is executing command: %s" "$from" "$action_line" send "$action_line" ;; join) for channel in "${action_args[@]}"; do join "$channel" privmsg "$to" "joined $channel" done ;; reload) if [[ -v markov_chains[@] ]]; then for key in "${!enabled[@]}"; do recording+=",$key=${enabled[$key]}" done export RECORDING=${recording#,} fi reload_vars=( nick ident host level log log_fd alarm_pid tls_pid in_sock out_sock sock_dir sys_root fact_root dev trigger registered keep_trying desired_nick to overwrite ) for env_var in "${reload_vars[@]}"; do export "${env_var^^}"="${!env_var}" done if (( SECONDS > LORE_LONGEST_LIFE )); then export LORE_LONGEST_LIFE=$SECONDS fi privmsg "$to" "reloading..." exec "$0" --reload "${original_args[@]}" ;; level) if is-log-level "${action_args[0]}"; then level=${action_args[0],,} privmsg "$to" "log level is now set to $level" else privmsg "$to" "${action_args[0]} is not a valid logging level" fi ;; dev) if [[ $dev = yes ]]; then dev=no privmsg "$to" "developer mode disabled" else dev=yes privmsg "$to" "developer mode enabled" fi ;; dev\?) if [[ $dev = yes ]]; then privmsg "$to" "developer mode is enabled" else privmsg "$to" "developer mode is disabled" fi ;; trigger) trigger=${action_args[0]} privmsg "$to" "trigger is now '$trigger'" ;; cycle) privmsg "$to" "cycling channel $to" part "$to" "be right back!" join "$to" ;; msg) recipient=${action_line%% *} msg=${action_line#"$recipient"* } read -r msg <<< "$msg" privmsg "$recipient" "$msg" privmsg "$to" "sent message to $recipient" ;; dashboard|lore) privmsg "$to" "Legend began on $(printf '%(%c)T' "$LORE_START_TIME")" privmsg "$to" "This life began on $(printf '%(%c)T' "$LORE_THIS_RELOAD")" if (( LORE_LONGEST_LIFE > SECONDS )); then privmsg "$to" "Longest life so far was $(seconds "$LORE_LONGEST_LIFE")" else privmsg "$to" "Longest life so far is $(seconds "$SECONDS") (this one)" fi if (( LORE_LIVES > 1 )); then privmsg "$to" "Lived $LORE_LIVES times" else privmsg "$to" "Lived $LORE_LIVES time" fi esac } hook_post_433_alternick() { if [[ -z $desired_nick && $registered = no ]]; then desired_nick=${args[1]} keep_trying=yes fi } hook_post_NICK_alternick() { if [[ ${args[0]} = "$desired_nick" ]]; then keep_trying=no info "obtained nick %s" "$desired_nick" fi } hook_cmd_limes() { local limes case $action in uplime) (( limes = (RANDOM % 42) + 1 )) if [[ $from = Time-Warp ]]; then limes=42 fi (( limeification = (limes * 100) / 42 )) privmsg "$to" "$limes limes (limes to $limeification%)" esac } ### # driver ### if [[ $reload = yes ]]; then if [[ -v TO ]]; then privmsg "$TO" done. unset TO else info done. fi else registered=no info "rowbot's pid is %d" "$BASHPID" nick "$nick" user "$ident" "$realname" fi # Always available: # - from: name of the entity sending the message # - ident: username of the entity sending the message # - host: host of the entity sending the message # - cmd: IRC command or numeric # - args: array of arguments to the command # Available if `cmd` is set to "privmsg": # - to: location to send the message back to # Additionally, if args[-1] starts with the trigger: # - action: directive specified by the sender # - action_line: data sent by the sender without the trigger or action # - action_args: words sent by the sender without the trigger or action while recv line; do args=( ) orig_line=$line if [[ ${line:0:1} = : ]]; then src=${line%% *} src=${src#:} line=${line#:"$src"} line=${line# } from=${src%@*} ident=${from#*!} from=${from%!*} host=${src#*@} fi cmd=${line%% *} line=${line#"$cmd"} line=${line# } while [[ $line ]]; do if [[ ${line:0:1} = : ]]; then args+=( "${line:1}" ) line="" else arg=${line%% *} args+=( "$arg" ) line=${line#"$arg"} line=${line# } fi done is_action=no if [[ ${cmd^^} = PRIVMSG ]]; then to=${args[0]} last=${args[-1]} if [[ ${to:0:1} != \# ]]; then to=$from fi if [[ $last = "$trigger"* ]]; then is_action=yes bot_score=$(is-bot) action=${last#"$trigger"} action=${action%% *} action_line=${last#"$trigger$action" } read -r action_line <<< "$action_line" read -ra action_args <<< "$action_line" debug "bot score: %d" "$bot_score" fi fi skip_handler=0 while IFS= read -r hook; do "$hook" (( skip_handler |= $? )) done < <(compgen -A function "hook_pre_${cmd^^}_") if has "on_${cmd^^}"; then if (( ! skip_handler )); then "on_${cmd^^}" else debug "handler for %s was skipped" "${cmd^^}" fi else warn "unhandled line: %s" "$orig_line" fi while IFS= read -r hook; do "$hook" done < <(compgen -A function "hook_post_${cmd^^}_") if [[ $is_action = yes ]] && (( bot_score < 40 )); then while IFS= read -r hook; do "$hook" done < <(compgen -A function "hook_cmd_") fi unset to done