rowbot/rowbot

1288 lines
25 KiB
Bash
Executable File

#!/usr/bin/env bash
###
# feature switch toggling
###
shopt -s dotglob extglob nullglob
stty -echoctl
###
# utility and helper functions
###
# cerealizers
put_array() {
# The variable named array is a nameref to an array
# shellcheck disable=SC2178
declare -n array=$1
declare -n scalar=ARR_${1^^}
local key
for key in "${!array[@]}"; do
scalar+=${#key}:$key${array[$key]}$'\n'
done
scalar=${scalar%$'\n'}
export "ARR_${1^^}"
}
get_array() {
declare -n scalar=ARR_${1^^}
declare -n array=$1
local entry idx_size
while read -r entry; do
idx_size=${entry%%:*} entry=${entry#"$idx_size":}
array[${entry:0:idx_size}]=${entry:idx_size}
done <<< "$scalar"
}
is_good_variable() {
[[ $1 =~ ^[A-Za-z_][A-Za-z0-9_]+$ ]]
}
# code reloading helpers
get_option() {
local var_name=${1//-/_}
local env_var=${var_name^^}
if [[ ! -v config[$1] || -v OPT_OW ]]; then
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
fi
}
is_parent() {
(( BASHPID == $$ ))
}
is_reloaded() {
[[ $RELOADED = yes ]] || (( LORE_LIVES > 1 ))
}
# message classification
is_action() {
[[ ${msg[cmd]} = PRIVMSG && ${msg_args[-1]:0:${#config[trigger]}} = "${config[trigger]}" ]]
}
is_chan() {
[[ ${msg_args[0]:0:1} = \# ]]
}
# cryptographically secure (almost maybe) pseudo random number utilities
random() {
local min=$1 max=$2
printf %d "$(( (RANDOM % max) + min ))"
}
any_file() {
local files=( "${1:-.}"/* ) max idx
if (( ${#files[@]} > 1 )); then
(( max = ${#files[@]} - 1 ))
idx=$(random 0 "$max")
printf %s "${files[$idx]}"
fi
}
# misc
is_running () {
kill -0 "$1" 2>/dev/null
}
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}"
}
has() {
if (( $# )); then
hash "$1" 2>/dev/null
else
return 1
fi
}
run_callbacks() {
if (( ! $# )); then
return 1
fi
local status=0 filter=$1
shift
while IFS= read -r; do
"$REPLY" "$@"
(( status |= $? ))
done < <(compgen -A function "$filter")
return "$status"
}
prints() {
# This is a wrapper around printf so the format string isn't known ahead of
# time.
# shellcheck disable=SC2059
printf "$@"
if [[ -t 1 ]]; then
printf \\n
fi
}
# The variables are checked dynamically
# shellcheck disable=SC2034
seconds() {
local day hour minute second span time
(( day = $1 / 60 / 60 / 24 ))
(( hour = $1 / 60 / 60 % 24 ))
(( minute = $1 / 60 % 60 ))
(( second = $1 % 60 ))
for span in day hour minute second; do
if (( ${!span} )); then
if [[ $time ]]; then
time+=", "
fi
time+="${!span} $span"
if (( ${!span} > 1 )); then
time+=s
fi
fi
done
prints %s "$time"
}
url() {
if [[ ${config[tls]} = no ]]; then
printf irc://
else
printf ircs://
fi
prints %s:%s "${config[server]}" "${config[port]}"
}
b64_encode() {
local idx=0 numerics table_idxs table_idx encoded
local table=( {A..Z} {a..z} {0..9} + / )
for (( ; idx < ${#1}; idx+=3 )); do
read -ra numerics < <(
printf '%d %d %d\n' "'${1:idx:1}" "'${1:idx+1:1}" "'${1:idx+2:1}"
)
(( table_idxs[0] = numerics[0] >> 2 ))
(( table_idxs[1] = (((numerics[0] & 0x03) << 6) | (numerics[1] & 0xF0) >> 2) >> 2 ))
(( table_idxs[2] = (((numerics[1] & 0x0F) << 4) | (numerics[2] & 0xC0) >> 4) >> 2 ))
(( table_idxs[3] = numerics[2] & 0x3F ))
for table_idx in "${table_idxs[@]}"; do
encoded+=${table[$table_idx]}
done
done
if (( ${#1} % 3 == 1 )); then
encoded=${encoded::-2}==
elif (( ${#1} % 3 == 2 )); then
encoded=${encoded::-1}=
fi
prints %s "$encoded"
}
###
# 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
###
# 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_trace() {
LOG_LEVEL=trace log "$@"
}
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=( [trace]=1 [debug]=2 [info]=3 [warn]=4 [error]=5 )
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
}
###
# bootup/shutdown sequence
###
on_sys_first_001_bootup() {
log_info "rowbot's pid is %d" "$$"
}
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]}"
log_trace "storing config key %s with value %s" "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"
log_trace "getting config key %s with value %s" "CONFIG_${setting_name^^}" "${config[$setting]}"
done < <(compgen -e CONFIG_)
}
on_sys_exit_999_bootup() {
log_info "There's a lot of beauty in ordinary things. Isn't that kind of the point?"
}
###
# net code
###
net_recv() {
declare -n sock_line=$1
IFS= read -ru "$in_sock" "$1"
sock_line=${sock_line%$'\r'}
log_trace "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_trace "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
log_info "rowbot is connecting to %s" "$(url)"
if [[ ${config[tls]} = no ]]; then
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
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() {
log_info "rowbot is closing the connection to %s" "$(url)"
if [[ ${config[tls]} = no ]]; then
exec {irc_sock}>&-
else
if [[ -v tls_pid ]] && is_running "$tls_pid"; then
kill -STOP "$tls_pid"
fi
rm -rf -- "$sock_dir"
fi
}
###
# annoyatron900 - keep alive process
###
annoyatron900() {
irc_ping "row your bot gently down the stream"
run_callbacks annoyatron900_
}
on_sys_before_999_annoyatron900() {
if [[ -v alarm_pid ]]; then
export ALARM_PID=$alarm_pid
fi
}
on_sys_after_999_annoyatron900() {
trap annoyatron900 USR1
if [[ -v ALARM_PID ]]; then
alarm_pid=$ALARM_PID
fi
}
on_sys_register_999_annoyatron900() {
while true; do
read -rt 10 </dev/zero
kill -USR1 "$$"
done &
alarm_pid=$!
trap annoyatron900 USR1
log_debug "process %d is being annoying" "$alarm_pid"
}
on_sys_exit_997_annoyatron900() {
log_debug "shutting down annoyatron900"
if [[ -v alarm_pid ]] && is_running "$alarm_pid"; then
kill -STOP "$alarm_pid"
fi
}
###
# register with the server
###
on_sys_first_003_welcome() {
get_option nick rowbot-dev
get_option ident rowbot
get_option realname rowbot
log_debug "registering with the server"
irc_nick "${config[nick]}"
irc_user "${config[ident]}" "${config[realname]}"
}
on_sys_init_999_welcome() {
get_option chan ""
declare -gA isupport
}
on_msg_005_welcome() {
local param key value
for param in "${msg_args[@]:1:${#msg_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
log_trace "isupport: %s = %s" "$key" "$value"
done
}
on_sys_before_001_welcome() {
put_array isupport
}
on_sys_after_999_welcome() {
get_array isupport
}
on_sys_register_999_welcome() {
if [[ ${config[chan]} ]]; then
irc_join "${config[chan]}"
fi
}
###
# magic required to make privmsg work
###
on_sys_register_001_privmagic() {
config[nick]=${msg_args[0]}
irc_who "${config[nick]}" %%uht,42
}
on_msg_354_privmagic() {
if (( msg_args[1] == 42 )); then
log_debug "received the identifying who"
config[ident]=${msg_args[2]} config[host]=${msg_args[3]}
fi
}
on_msg_396_privmagic() {
config[host]=${msg_args[1]}
log_debug "config host has been changed to %s" "${msg_args[1]}"
}
###
# irc receive handlers
###
irc_on_ERROR() {
log_error "${msg_args[0]}"
exit
}
irc_on_JOIN() {
log_info "%s has joined %s" "${msg[from]}" "${msg_args[0]}"
}
irc_on_KICK() {
if (( ${#msg_args[@]} == 3 )); then
log_info "%s has kicked %s from %s: %s" "${msg[from]}" "${msg_args[1]}" "${msg_args[0]}" "${msg_args[-1]}"
else
log_info "%s has kicked %s from %s" "${msg[from]}" "${msg_args[1]}" "${msg_args[0]}"
fi
}
irc_on_MODE() {
if (( ${#msg_args[@]} == 2 )); then
log_info "%s sets mode(s) %s on %s" "${msg[from]}" "${msg_args[1]}" "${msg_args[0]}"
elif (( ${#msg_args[@]} > 2 )); then
log_info "%s: %s sets mode(s) %s" "${msg_args[0]}" "${msg[from]}" "${msg_args[*]:1}"
fi
}
irc_on_NICK() {
log_info "%s has changed their name to %s" "${msg[from]}" "${msg_args[0]}"
}
irc_on_NOTICE() {
log_info "[%s/%s] %s" "${msg[from]}" "${msg_args[0]}" "${msg_args[1]}"
}
irc_on_PART() {
if (( ${#msg_args[@]} > 1 )); then
log_info "%s has left %s: %s" "${msg[from]}" "${msg_args[0]}" "${msg_args[1]}"
else
log_info "%s has left %s" "${msg[from]}" "${msg_args[0]}"
fi
}
irc_on_PING() {
irc_pong "${msg_args[1]}"
log_trace "received ping: %s" "${msg_args[0]}"
}
irc_on_PONG() {
log_trace "received pong: %s" "${msg_args[1]}"
}
irc_on_PRIVMSG() {
log_info "<%s/%s> %s" "${msg[from]}" "${msg_args[0]}" "${msg_args[1]}"
}
irc_on_TOPIC() {
log_info "%s has changed the topic for %s: %s" "${msg[from]}" "${msg_args[0]}" "${msg_args[1]}"
}
irc_on_QUIT() {
log_info "%s has disconnected: %s" "${msg[from]}" "${msg_args[0]}"
}
irc_on_001() {
log_info %s "${msg_args[1]}"
run_callbacks on_sys_register_
run_callbacks on_register_
}
irc_on_002() {
log_info %s "${msg_args[1]}"
}
irc_on_003() {
log_info %s "${msg_args[1]}"
}
irc_on_004() {
log_debug "%s " "${msg_args[@]:1}"
}
irc_on_005() {
log_debug "received isupport specification: ${msg_args[*]:1}"
}
irc_on_250() {
log_info %s "${msg_args[1]}"
}
irc_on_251() {
log_info %s "${msg_args[1]}"
}
irc_on_252() {
log_info "There are %d operators online" "${msg_args[1]}"
}
irc_on_253() {
log_info "There are %d unknown connections" "${msg_args[1]}"
}
irc_on_254() {
log_info "There are %d channels formed" "${msg_args[1]}"
}
irc_on_255() {
log_info %s "${msg_args[1]}"
}
irc_on_265() {
log_info %s "${msg_args[3]}"
}
irc_on_266() {
log_info %s "${msg_args[3]}"
}
irc_on_315() {
log_debug "end of WHO for %s" "${msg_args[1]}"
}
irc_on_332() {
log_info "topic for %s is %s" "${msg_args[1]}" "${msg_args[2]}"
}
irc_on_333() {
local date
printf -v date '%(%c)T' "${msg_args[3]}"
log_info "topic for %s set by %s at %s" "${msg_args[1]}" "${msg_args[2]}" "$date"
}
irc_on_353() {
log_info "members of %s: %s" "${msg_args[2]}" "${msg_args[3]}"
}
irc_on_354() {
log_debug "who: %s" "${msg_args[*]}"
}
irc_on_366() {
log_debug "%s: end of NAMES list" "${msg_args[1]}"
}
irc_on_372() {
log_info %s "${msg_args[1]}"
}
irc_on_375() {
log_debug %s "${msg_args[1]}"
}
irc_on_376() {
log_debug %s "${msg_args[1]}"
}
irc_on_396() {
log_info "%s %s" "${msg_args[1]}" "${msg_args[-1]}"
}
irc_on_401() {
log_info "%s is unavailable: %s" "${msg_args[1]}" "${msg_args[-1]}"
}
irc_on_433() {
log_info "somebody is already using %s" "${msg_args[1]}"
}
irc_on_438() {
log_error "%s couldn't change their nick to %s: %s" "${msg_args[1]}" "${msg_args[2]}" "${msg_args[-1]}"
}
irc_on_473() {
log_error "%s: %s" "${msg_args[1]}" "${msg_args[2]}"
}
irc_on_718() {
log_info "%s %s" "${msg_args[1]}" "${msg_args[-1]}"
}
###
# irc send handlers
###
irc_accept() {
net_send "ACCEPT $1"
}
irc_join() {
local chans
printf -v chans %s, "$@"
net_send "JOIN %s" "${chans%,}"
}
irc_mode() {
if (( $# == 1 )); then
net_send "MODE ${config[nick]} ${config[modes]}"
fi
}
irc_nick() {
net_send "NICK :%s" "$1"
}
irc_notice() {
local msg=$2 msg_len
if [[ -v config[host] ]]; then
(( msg_len = 494 - (${#config[nick]} + ${#config[ident]} + ${#config[host]} + ${#1}) ))
log_trace "max message length for NOTICE is %d" "$msg_len"
while (( ${#msg} > msg_len )); do
net_send "NOTICE %s :"$'\xe2\x80\x8b'"%s" "$1" "${msg:0:$msg_len}"
log_info "[%s/%s] %s" "${config[nick]}" "$1" "${msg:0:$msg_len}"
msg=${msg:$msg_len}
done
fi
net_send "NOTICE %s :"$'\xe2\x80\x8b'"%s" "$1" "$msg"
log_info "[%s/%s] %s" "${config[nick]}" "$1" "$msg"
}
irc_part() {
if (( $# )); then
if (( $# > 1 )); then
net_send "PART $1 :$2"
else
net_send "PART $1"
fi
fi
}
irc_ping() {
net_send "PING :%s" "$1"
}
irc_pong() {
net_send "PONG %s" "$1"
}
irc_privmsg() {
local msg=$2 msg_len
if [[ -v config[host] ]]; then
(( msg_len = 493 - (${#config[nick]} + ${#config[ident]} + ${#config[host]} + ${#1}) ))
log_trace "max message length for PRIVMSG is %d" "$msg_len"
while (( ${#msg} > msg_len )); do
net_send "PRIVMSG %s :"$'\xe2\x80\x8b'"%s" "$1" "${msg:0:$msg_len}"
log_info "<%s/%s> %s" "${config[nick]}" "$1" "${msg:0:$msg_len}"
msg=${msg:$msg_len}
done
fi
net_send "PRIVMSG %s :"$'\xe2\x80\x8b'"%s" "$1" "$msg"
log_info "<%s/%s> %s" "${config[nick]}" "$1" "$msg"
}
irc_part() {
if (( $# )); then
if (( $# > 1 )); then
net_send "PART $1 :$2"
else
net_send "PART $1"
fi
fi
}
irc_quit() {
if (( $# )); then
net_send "QUIT :%s" "$1"
else
net_send QUIT
fi
}
irc_user() {
net_send "USER %s 0 * :%s" "$1" "$2"
}
irc_who() {
if (( $# > 1 )); then
net_send "WHO %s %s" "$1" "$2"
else
net_send "WHO %s" "$1"
fi
}
###
# plugin api
###
plugin_reg() {
if ! is_good_variable irc_plugin_array_"$1"; then
return 1
fi
declare -n plugins=irc_plugin_array_"$1"
local plugin
if [[ -v plugins ]]; then
for plugin in "${plugins[@]}"; do
if [[ $plugin = "$2" ]]; then
return 1
fi
done
else
plugins=( )
fi
plugins+=( "$2" )
}
plugin_run() {
# This is a false positive.
# shellcheck disable=SC2178
if is_good_variable irc_plugin_array_"$1" && [[ -v irc_plugin_array_"$1" ]]; then
declare -n plugins=irc_plugin_array_"$1"
shift
local plugin
for plugin in "${plugins[@]}"; do
"$plugin" "$@"
done
else
run_callbacks plugin_not_found_
fi
}
###
# plugins
###
# lime-o-meter
limeometer() {
# We don't care about failures here, for better or for worse.
# shellcheck disable=SC2155
local limes=$(random 1 42) limeification
if [[ ${msg[from]} = Time-Warp ]]; then
limes=42
fi
(( limeification = (limes * 100) / 42 ))
irc_privmsg "${msg[to]}" "$limes limes (limes to $limeification%)"
}
on_init_limeometer() {
plugin_reg uplime limeometer
}
# nonlogger
on_early_msg_PRIVMSG_nolog() {
if [[ ${msg_words[0]} = *nolog* ]]; then
log_info "this message was redacted"
return 1
else
return 0
fi
}
# debugger
debugger_toggle() {
if [[ $- = *x* ]]; then
irc_privmsg "${msg[to]}" "disabling debug mode"
set +x
else
irc_privmsg "${msg[to]}" "enabling debug mode"
set -x
fi
}
on_init_debugger() {
plugin_reg debug debugger_toggle
}
on_before_debugger() {
if [[ $- = *x* ]]; then
export SET_X=yes
fi
}
on_after_debugger() {
if [[ -v SET_X && $SET_X = yes ]]; then
set -x
unset SET_X
fi
}
# system administration facts
get_sysfact() {
local idx
(( idx = RANDOM % ${#sysfacts[@]} ))
irc_privmsg "${msg[to]}" "sysfact #$(( idx + 1 )): ${sysfacts[$idx]}"
}
on_init_sysfacts() {
sysfacts=(
"Use the \`rm\` command to read manuals. Use \`rm -rf\` to read manuals really fast."
"When in doubt, use HTTP as a transport layer."
"TLS is only needed in the United States, since the NSA doesn't monitor anywhere else."
Linux.
"Your local computer group can teach you all of the computer tricks you see on TV."
"When people talk about headless servers, they mean deleting head(1) from it."
"Store your passwords in plaintext, so a user can recover one easily if they forget it."
"Cron and screen make the best process manager."
)
plugin_reg sysfact get_sysfact
}
# alternick tracking
on_init_alternick() {
get_option registered no
}
on_register_alternick() {
config[registered]=yes
}
on_early_msg_433_alternick() {
if [[ ${config[registered]} = yes ]]; then
log_debug "somebody is already using ${config[nick]}"
return 1
fi
}
on_msg_433_alternick() {
if [[ ${config[registered]} = no ]]; then
log_info "using nick ${config[nick]}_"
irc_nick "${config[nick]}_"
desired_nick=${config[nick]}
fi
}
annoyatron900_alternick() {
if [[ $desired_nick ]]; then
irc_nick "$desired_nick"
fi
}
on_msg_NICK_alternick() {
if [[ ${msg[from]} = "${config[nick]}" ]]; then
desired_nick=
log_info "got desired nick %s!" "${msg_args[-1]}"
config[nick]=${msg_args[-1]}
fi
}
# factoids
factoids_cmd_is() {
local fact_name=${action_line%% *}
if [[ $fact_name = "$action_line" || $fact_name = "$action_line " ]]; then
irc_privmsg "${msg[to]}" "Can you repeat that?"
return 0
fi
local fact_value=${action_line#"$fact_name"}
fact_value=${fact_value# }
irc_privmsg "${msg[to]}" "I'm sure I'll remember that."
mkdir -p "${config[fact-root]}"/"${msg[to]}"
printf %s "$fact_value" > "${config[fact-root]}"/"${msg[to]}"/"$fact_name"
}
factoids_cmd_isnt() {
if [[ -f ${config[fact-root]}/${msg[to]}/$action_line ]]; then
irc_privmsg "${msg[to]}" "I forgot what that was anyways."
rm -f "${config[fact-root]}"/"${msg[to]}"/"$action_line"
fi
}
factoids_cmd_ls() {
local facts=( "${config[fact-root]}"/"${msg[to]}"/* )
irc_privmsg "${msg[to]}" "${facts[*]##*/}"
}
plugin_not_found_factoids() {
if [[ ${config[fact-root]} && -f ${config[fact-root]}/${msg[to]}/$action ]]; then
# The exit status isn't important here.
# shellcheck disable=SC2155
local fact=$(<"${config[fact-root]}"/"${msg[to]}"/"$action")
local target idx fmt_fact
if [[ ${action_args[0]} = \> ]] && (( ${#action_args[@]} > 1 )); then
target=${action_args[-1]}
else
target=${msg[from]}
fi
for (( idx = 0; idx < ${#fact}; idx += 1 )); do
if [[ ${fact:idx:1} = % ]] && (( idx + 1 < ${#fact} )); then
(( idx += 1 ))
case ${fact:idx:1} in
t)
fmt_fact+=$target
;;
c)
fmt_fact+=${msg[from]}
;;
r)
fmt_fact+=$(random 0 100)
;;
*)
fmt_fact+=${fact:idx:1}
esac
else
fmt_fact+=${fact:idx:1}
fi
done
irc_privmsg "${msg[to]}" "$target: $fmt_fact"
fi
}
on_init_factoids() {
get_option fact-root ""
if [[ ${config[fact-root]} && -d ${config[fact-root]} ]]; then
plugin_reg is factoids_cmd_is
plugin_reg isnt factoids_cmd_isnt
plugin_reg ls factoids_cmd_ls
fi
}
# request default modes
on_init_mode_getter() {
get_option modes wigR
}
on_register_mode_getter() {
if [[ ${config[modes]:0:1} = @(+|-) ]]; then
config[modes]=+${config[modes]}
fi
irc_mode "${config[modes]}"
irc_accept "${config[owner]}"
}
###
# atexit(3)-style 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_trace "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# }
msg[cmd]=${msg[cmd]^^}
log_trace "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=
read -ra msg_words <<< "${msg_args[-1]}"
log_trace "parsed final argument %s" "${msg_args[-1]}"
else
arg=${line%% *} msg_args+=( "$arg" ) line=${line#"$arg"} line=${line# }
log_trace "parsed argument %s" "$arg"
fi
done
if [[ ${msg[cmd]} = @(PRIVMSG|NOTICE) ]]; then
msg[to]=${msg[from]}
if is_chan; then
msg[to]=${msg_args[0]}
fi
case ${msg_args[-1]} in
"["*"]")
(( msg[score] += 20 ))
;;
$'\xe2\x80\x8b'*)
(( msg[score] += 100 ))
esac
fi
log_trace "bot score is %d" "${msg[score]}"
if has irc_on_"${msg[cmd]}"; then
if run_callbacks "on_early_msg_${msg[cmd]}_"; then
irc_on_"${msg[cmd]}"
else
log_debug "handler for %s was skipped" "${msg[cmd]}"
fi
run_callbacks "on_msg_${msg[cmd]}_"
else
log_warn "unhandled line: %s" "${msg[original]}"
fi
if is_action; then
action=${msg_args[-1]#"${config[trigger]}"} action=${action%% *}
action_line=${msg_args[-1]#"$trigger$action"}
read -r action_line <<< "$action_line"
read -ra action_args <<< "$action_line"
plugin_run "$action"
fi
done