1639 lines
34 KiB
Bash
Executable File
1639 lines
34 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
###
|
|
# feature switch toggling
|
|
###
|
|
|
|
shopt -s dotglob extglob nullglob
|
|
stty -echoctl
|
|
|
|
###
|
|
# utility and helper functions
|
|
###
|
|
|
|
# cerealizers
|
|
|
|
put_assoc_array() {
|
|
local key put_name=RB_AA_${1^^}
|
|
declare -n assoc_array=$1
|
|
declare -n scalar=$put_name
|
|
|
|
for key in "${!assoc_array[@]}"; do
|
|
scalar+=${#key},${#assoc_array[$key]}:$key${assoc_array[$key]}
|
|
done
|
|
|
|
log_trace "storing %s as %s=%s" "$1" "$put_name" "${scalar@Q}"
|
|
export "${put_name?}"
|
|
}
|
|
|
|
get_assoc_array() {
|
|
if [[ $1 && ! -v $1 ]]; then
|
|
declare -gA "$1"
|
|
fi
|
|
|
|
local key_len val_len key debug_str get_name=RB_AA_${1^^}
|
|
declare -n assoc_array=$1
|
|
declare -n scalar=$get_name
|
|
|
|
while [[ $scalar ]]; do
|
|
key_len=${scalar%%,*} val_len=${scalar#*,}
|
|
val_len=${val_len%%:*} scalar=${scalar#"$key_len","$val_len":}
|
|
assoc_array[${scalar:0:key_len}]=${scalar:key_len:val_len}
|
|
scalar=${scalar:key_len + val_len}
|
|
done
|
|
|
|
for key in "${!assoc_array[@]}"; do
|
|
debug_str+="[$key]=${assoc_array[$key]@Q} "
|
|
done
|
|
|
|
log_trace "retreiving %s as %s=(%s)" "$get_name" "$1" "$debug_str"
|
|
unset "$get_name"
|
|
}
|
|
|
|
put_array() {
|
|
local val put_name=RB_A_${1^^}
|
|
# The variable named array is a nameref to an array
|
|
# shellcheck disable=SC2178
|
|
declare -n array=$1
|
|
declare -n scalar=$put_name
|
|
|
|
for val in "${array[@]}"; do
|
|
scalar+=${#val}:$val
|
|
done
|
|
|
|
log_trace "storing %s as %s=%s" "$1" "$put_name" "${scalar@Q}"
|
|
export "${put_name?}"
|
|
}
|
|
|
|
get_array() {
|
|
local len val get_name=RB_A_${1^^}
|
|
declare -n array=$1
|
|
declare -n scalar=$get_name
|
|
|
|
while [[ $scalar ]]; do
|
|
len=${scalar%%:*} scalar=${scalar#"$len":}
|
|
val=${scalar:0:len} scalar=${scalar:len}
|
|
array+=( "$val" )
|
|
done
|
|
|
|
log_trace "retreiving %s as %s=(%s)" "$get_name" "$1" "${array[*]@Q}"
|
|
unset "$get_name"
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
# code reloading helpers
|
|
|
|
is_reloaded() {
|
|
[[ $RELOADED = yes ]] || (( RELOAD_COUNT ))
|
|
}
|
|
|
|
# message classification
|
|
|
|
is_action() {
|
|
# The only possible fail conditions are already checked for.
|
|
# shellcheck disable=SC2155
|
|
local trigger=$(state_get trigger)
|
|
[[ ${msg[cmd]} = PRIVMSG && ${msg_args[-1]:0:${#trigger}} = "$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
|
|
}
|
|
|
|
shuffle() {
|
|
local idx=0 spot tmp
|
|
declare -n rowbot_array=$1
|
|
|
|
for (( ; idx < ${#rowbot_array[@]}; idx += 1 )); do
|
|
spot=$(random 0 "${#rowbot_array[@]}")
|
|
tmp=${rowbot_array[$idx]}
|
|
rowbot_array[$idx]=${rowbot_array[$spot]}
|
|
rowbot_array[$spot]=$tmp
|
|
done
|
|
}
|
|
|
|
# process management and friends
|
|
|
|
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
|
|
}
|
|
|
|
is_running () {
|
|
kill -0 "$1" 2>/dev/null
|
|
}
|
|
|
|
# misc
|
|
|
|
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 NS=net QUIET="" state_get tls; then
|
|
printf ircs://
|
|
else
|
|
printf irc://
|
|
fi
|
|
|
|
# The only possible fail conditions are already checked for.
|
|
# shellcheck disable=SC2155
|
|
local server=$(NS=net state_get server) port=$(NS=net state_get port)
|
|
prints %s:%s "$server" "$port"
|
|
}
|
|
|
|
###
|
|
# Prepare rowbot's configuration
|
|
###
|
|
|
|
# parse command line arguments
|
|
|
|
declare -A config 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
|
|
|
|
mapfile -t old_set < <(compgen -v)
|
|
|
|
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
|
|
|
|
mapfile -t new_set < <(compgen -v)
|
|
|
|
for new in "${new_set[@]}"; do
|
|
found=0
|
|
|
|
for old in "${old_set[@]}"; do
|
|
if [[ $new = "$old" ]]; then
|
|
found=1
|
|
break
|
|
fi
|
|
done
|
|
|
|
if (( !found )); then
|
|
config[${new//_/-}]=${!new}
|
|
fi
|
|
done
|
|
|
|
for setting in "${!opts[@]}"; do
|
|
config[$setting]=${opts[$setting]}
|
|
done
|
|
|
|
while IFS= read -r setting; do
|
|
config[${setting,,}]=${!setting}
|
|
done < <(compgen -e)
|
|
|
|
# cleanup
|
|
|
|
unset key opts old_set file new_set new found old setting
|
|
|
|
###
|
|
# state management
|
|
###
|
|
|
|
state_manage() {
|
|
local managed found=0
|
|
declare -gA __rowbot_state_store_"$1"
|
|
|
|
if [[ $1 != global ]]; then
|
|
for managed in "${states_managed[@]}"; do
|
|
if [[ $managed = "$1" ]]; then
|
|
found=1
|
|
break
|
|
fi
|
|
done
|
|
|
|
if (( !found )); then
|
|
states_managed+=( "$1" )
|
|
fi
|
|
fi
|
|
}
|
|
|
|
state_resolve() {
|
|
local ns=${NS-global}
|
|
declare -n ns_config=__rowbot_state_store_"$ns"
|
|
state_manage "$ns"
|
|
|
|
# This is a false positive.
|
|
# shellcheck disable=SC2102
|
|
if [[ -v config[$ns-$1] ]]; then
|
|
ns_config[$1]=${config[$ns-$1]}
|
|
elif [[ -v config[$1] ]]; then
|
|
ns_config[$1]=${config[$1]}
|
|
elif [[ -v DEFAULT ]]; then
|
|
ns_config[$1]=$DEFAULT
|
|
elif [[ -v DEFAULT_N && $DEFAULT_N ]]; then
|
|
ns_config[$1]=$DEFAULT_N
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
state_put() {
|
|
local ns=${NS-global}
|
|
# The `ns_config` variable is a reference to an array
|
|
# shellcheck disable=SC2178
|
|
declare -n ns_config=__rowbot_state_store_"$ns"
|
|
state_manage "$ns"
|
|
ns_config[$1]=$2
|
|
}
|
|
|
|
state_get() {
|
|
# The `ns_config` variable is a reference to an array
|
|
# shellcheck disable=SC2178
|
|
declare -n ns_config=__rowbot_state_store_"${NS-global}"
|
|
|
|
if [[ -v ns_config[$1] ]]; then
|
|
if [[ ! -v QUIET || $QUIET = no ]]; then
|
|
printf %s "${ns_config[$1]}"
|
|
fi
|
|
|
|
if [[ ${ns_config[$1]} = no ]]; then
|
|
return 1
|
|
fi
|
|
elif [[ -v DEFAULT ]]; then
|
|
if [[ ! -v QUIET || $QUIET = no ]]; then
|
|
printf %s "$DEFAULT"
|
|
fi
|
|
|
|
if [[ $DEFAULT = no ]]; then
|
|
return 1
|
|
fi
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
state_keys() {
|
|
# The `ns_config` variable is a reference to an array
|
|
# shellcheck disable=SC2178
|
|
declare -n ns_config=__rowbot_state_store_"${NS-global}"
|
|
declare -n array_keys=${1-CONFIG_KEYS}
|
|
# The `array_keys` variable initializes a variable declared by the calling
|
|
# code.
|
|
# shellcheck disable=SC2034
|
|
array_keys=( "${!ns_config[@]}" )
|
|
}
|
|
|
|
state_has() {
|
|
local ns=${NS-global} found=1 managed
|
|
# The `ns_config` variable is a reference to an array
|
|
# shellcheck disable=SC2178
|
|
declare -n ns_config=__rowbot_state_store_"$ns"
|
|
|
|
for managed in "${!ns_config[@]}"; do
|
|
if [[ $managed = "$1" ]]; then
|
|
found=0
|
|
break
|
|
fi
|
|
done
|
|
|
|
return "$found"
|
|
}
|
|
|
|
on_sys_init_001_state() {
|
|
states_managed=( global )
|
|
}
|
|
|
|
on_sys_before_999_state() {
|
|
local managed
|
|
|
|
for managed in "${states_managed[@]}"; do
|
|
put_assoc_array __rowbot_state_store_"$managed"
|
|
done
|
|
|
|
put_array states_managed
|
|
}
|
|
|
|
on_sys_after_001_state() {
|
|
local managed
|
|
get_array states_managed
|
|
|
|
for managed in "${states_managed[@]}"; do
|
|
declare -gA __rowbot_state_store_"$managed"
|
|
get_assoc_array __rowbot_state_store_"$managed"
|
|
done
|
|
}
|
|
|
|
###
|
|
# logger
|
|
###
|
|
|
|
log() {
|
|
if NS=log state_has fd; then
|
|
# The only possible fail conditions are already checked for.
|
|
# shellcheck disable=SC2155
|
|
local level=$(NS=log state_get level) fd=$(NS=log state_get fd)
|
|
|
|
if (( log_levels[$level] <= log_levels[$LOG_LEVEL] )); then
|
|
printf "%s: $1\n" "${LOG_LEVEL^^}" "${@:2}" >&"$fd"
|
|
fi
|
|
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_005_log() {
|
|
declare -gA log_levels=( [trace]=1 [debug]=2 [info]=3 [warn]=4 [error]=5 )
|
|
NS=log DEFAULT=info state_resolve level
|
|
NS=log state_resolve log
|
|
NS=log DEFAULT=no state_resolve overwrite
|
|
local log_fd=1
|
|
|
|
if ! log_has_level "$(NS=log state_get level)"; then
|
|
die "%s is not a valid logging level" "$(NS=log state_get level)"
|
|
fi
|
|
|
|
if NS=log state_has log; then
|
|
# The only possible fail conditions are already checked for.
|
|
# shellcheck disable=SC2155
|
|
local log_file=$(NS=log state_get log)
|
|
|
|
if NS=log QUIET="" state_get overwrite; then
|
|
exec {log_fd}>"$log_file"
|
|
else
|
|
exec {log_fd}>>"$log_file"
|
|
fi
|
|
fi
|
|
|
|
NS=log DEFAULT=$log_fd state_resolve fd
|
|
log_trace "rowbot is prepared for logging"
|
|
}
|
|
|
|
on_sys_before_999_log() {
|
|
if NS=log state_has fd; then
|
|
# The only possible fail condition is already checked for.
|
|
# shellcheck disable=2155
|
|
local fd=$(NS=log state_get fd)
|
|
|
|
if (( fd != 1 )); then
|
|
log_debug "shutting logger down for reload"
|
|
exec {fd}>&-
|
|
fi
|
|
fi
|
|
}
|
|
|
|
on_sys_exit_999_log() {
|
|
if NS=log state_has fd; then
|
|
# The only possible fail condition is already checked for.
|
|
# shellcheck disable=2155
|
|
local fd=$(NS=log state_get fd)
|
|
|
|
if (( fd != 1 )); then
|
|
log_debug "shutting logger down for good"
|
|
exec {fd}>&-
|
|
fi
|
|
fi
|
|
}
|
|
|
|
###
|
|
# bootup/shutdown sequence
|
|
###
|
|
|
|
on_sys_first_010_bootup() {
|
|
log_info "rowbot's pid is %d" "$$"
|
|
}
|
|
|
|
on_sys_before_995_bootup() {
|
|
log_debug "storing the config"
|
|
put_assoc_array config
|
|
}
|
|
|
|
on_sys_after_005_bootup() {
|
|
log_debug "retreiving the config"
|
|
get_assoc_array config
|
|
}
|
|
|
|
on_exit_zzz_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
|
|
# The only possible fail conditions are already checked for.
|
|
# shellcheck disable=SC2155
|
|
local in_sock=$(NS=net state_get in-sock)
|
|
IFS= read -ru "$in_sock" "$1"
|
|
sock_line=${sock_line%$'\r'}
|
|
log_trace "received line: %s" "$sock_line"
|
|
}
|
|
|
|
net_send() {
|
|
# The only possible fail conditions are already checked for.
|
|
# shellcheck disable=SC2155
|
|
local fmt out_sock=$(NS=net state_get out-sock)
|
|
# 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_015_net() {
|
|
NS=net DEFAULT=irc.libera.chat state_resolve server
|
|
NS=net DEFAULT=no state_resolve tls
|
|
|
|
if NS=net QUIET="" state_get tls; then
|
|
NS=net DEFAULT=6697 state_resolve port
|
|
NS=net state_resolve client-cert
|
|
else
|
|
NS=net DEFAULT=6667 state_resolve port
|
|
fi
|
|
}
|
|
|
|
on_sys_first_015_net() {
|
|
local conn_args irc_sock
|
|
# The only possible fail conditions are already checked for.
|
|
# shellcheck disable=SC2155
|
|
local server=$(NS=net state_get server) port=$(NS=net state_get port)
|
|
log_info "rowbot is connecting to %s" "$(url)"
|
|
|
|
if NS=net QUIET="" state_get tls; then
|
|
log_debug "requesting tls connection"
|
|
|
|
if ! has socat; then
|
|
die "please install socat to use tls with rowbot."
|
|
fi
|
|
|
|
# The only possible fail conditions are already checked for.
|
|
# shellcheck disable=SC2155
|
|
local sock_dir=$(mktemp -d)
|
|
log_debug "socket directory is %s" "$sock_dir"
|
|
NS=net state_put sock-dir "$sock_dir"
|
|
mkfifo "$sock_dir"/rowbot-{in,out}.sock
|
|
|
|
if NS=net state_has client-cert; then
|
|
# The only possible fail conditions are already checked for.
|
|
# shellcheck disable=SC2155
|
|
local client_cert=$(NS=net state_get client-cert)
|
|
log_debug "using a client certificate with the tls connection"
|
|
|
|
if [[ ! -f $client_cert ]]; then
|
|
die "client certificate not found: %s" "$client_cert"
|
|
elif [[ ! -r $client_cert ]]; then
|
|
die "client certificate is not readable"
|
|
fi
|
|
|
|
log_debug "client certificate file was found"
|
|
conn_args=OPENSSL:$server:$port,cert=$client_cert
|
|
else
|
|
log_debug "not using a client certificate for tls"
|
|
conn_args=OPENSSL:$server:$port
|
|
fi
|
|
|
|
log_debug "socat connection settings are %s" "$conn_args"
|
|
socat "$conn_args" - <"$sock_dir"/rowbot-in.sock >"$sock_dir"/rowbot-out.sock &
|
|
local tls_pid=$! out_sock in_sock
|
|
exec {out_sock}>"$sock_dir"/rowbot-in.sock {in_sock}<"$sock_dir"/rowbot-out.sock
|
|
NS=net state_put tls-pid "$tls_pid"
|
|
NS=net state_put out-sock "$out_sock"
|
|
NS=net state_put in-sock "$in_sock"
|
|
log_debug "process %d is handling tls" "$tls_pid"
|
|
else
|
|
log_debug "requesting plaintext connection"
|
|
exec {irc_sock}<>/dev/tcp/"$server"/"$port"
|
|
NS=net state_put in-sock "$irc_sock"
|
|
NS=net state_put out-sock "$irc_sock"
|
|
fi
|
|
|
|
log_debug "connection established"
|
|
}
|
|
|
|
on_sys_exit_998_net() {
|
|
log_info "rowbot is closing the connection to %s" "$(url)"
|
|
|
|
# The only possible fail conditions are already checked for.
|
|
# shellcheck disable=SC2155
|
|
if NS=net state_has tls; then
|
|
if NS=net state_has tls-pid; then
|
|
local tls_pid=$(NS=net state_get tls-pid)
|
|
|
|
if is_running "$tls_pid"; then
|
|
log_debug "stopping the tls process"
|
|
kill -STOP "$tls_pid"
|
|
else
|
|
log_debug "tls process is not running"
|
|
fi
|
|
fi
|
|
|
|
log_debug "removing the socket directory"
|
|
rm -rf -- "$(NS=net state_get sock-dir)"
|
|
else
|
|
local irc_sock=$(NS=net state_get in-sock)
|
|
exec {irc_sock}>&-
|
|
fi
|
|
|
|
log_debug "connection closed"
|
|
}
|
|
|
|
###
|
|
# annoyatron900 - keep alive process
|
|
###
|
|
|
|
annoyatron900() {
|
|
irc_ping "row your bot gently down the stream"
|
|
run_callbacks annoyatron900_
|
|
}
|
|
|
|
on_sys_init_900_annoyatron900() {
|
|
trap annoyatron900 USR1
|
|
}
|
|
|
|
on_sys_register_999_annoyatron900() {
|
|
while true; do
|
|
read -rt 10 </dev/zero
|
|
|
|
if is_running "$$"; then
|
|
kill -USR1 "$$"
|
|
else
|
|
kill -INT "$BASHPID"
|
|
fi
|
|
done &
|
|
|
|
local alarm_pid=$!
|
|
NS=annoyatron900 state_put alarm-pid "$alarm_pid"
|
|
log_debug "process %d is being annoying" "$alarm_pid"
|
|
}
|
|
|
|
on_sys_exit_997_annoyatron900() {
|
|
log_debug "shutting down annoyatron900"
|
|
|
|
if NS=annoyatron900 state_has alarm-pid; then
|
|
# The only possible fail conditions are already checked for.
|
|
# shellcheck disable=SC2155
|
|
local alarm_pid=$(NS=annoyatron900 state_get alarm-pid)
|
|
|
|
if is_running "$alarm_pid"; then
|
|
kill -STOP "$alarm_pid"
|
|
fi
|
|
|
|
log_debug "annoyatron900 is dead"
|
|
else
|
|
log_debug "annoyatron900 was not running"
|
|
fi
|
|
}
|
|
|
|
###
|
|
# register with the server
|
|
###
|
|
|
|
on_sys_first_020_enroll() {
|
|
NS=enroll state_resolve caps
|
|
NS=enroll state_resolve pass
|
|
NS=irc DEFAULT=rowbot-dev state_resolve nick
|
|
NS=irc DEFAULT=rowbot state_resolve ident
|
|
NS=irc DEFAULT=rowbot state_resolve realname
|
|
|
|
log_debug "beginning irc registration handshake"
|
|
|
|
if NS=enroll state_has caps; then
|
|
log_debug "requesting list of available capabilities"
|
|
irc_cap ls
|
|
fi
|
|
|
|
if NS=enroll state_has pass; then
|
|
log_debug "sending account password"
|
|
irc_pass "$(NS=enroll state_get pass)"
|
|
fi
|
|
|
|
log_debug "sending registration data"
|
|
irc_nick "$(NS=irc state_get nick)"
|
|
irc_user "$(NS=irc state_get ident)" "$(NS=irc state_get realname)"
|
|
}
|
|
|
|
on_msg_CAP_enroll() {
|
|
local avail_cap{s,} desired_cap{s,} caps mechanism advertised
|
|
|
|
if NS=enroll state_has caps; then
|
|
case ${msg_args[1]^^} in
|
|
LS)
|
|
if ! NS=enroll QUIET="" state_get requested-caps; then
|
|
NS=avail_caps state_keys avail_caps
|
|
read -ra desired_caps < <(NS=enroll state_get caps)
|
|
|
|
# The `avail_caps` array gets initialized in the state_keys function.
|
|
# shellcheck disable=SC2154
|
|
for avail_cap in "${avail_caps[@]}"; do
|
|
for desired_cap in "${desired_caps[@]}"; do
|
|
if [[ $avail_cap = "$desired_cap" ]]; then
|
|
caps+=( "$avail_cap" )
|
|
NS=caps state_put "$avail_cap" ""
|
|
fi
|
|
done
|
|
done
|
|
|
|
irc_cap req "${caps[@]}"
|
|
NS=enroll state_put requested-caps yes
|
|
fi
|
|
;;
|
|
NAK)
|
|
irc_cap end
|
|
NS=enroll state_put have-caps no
|
|
;;
|
|
ACK)
|
|
NS=enroll state_put have-caps yes
|
|
|
|
if NS=caps state_has sasl; then
|
|
if NS=enroll state_has pass; then
|
|
mechanism=plain
|
|
elif NS=net QUIET="" state_get tls && NS=net state_has client-cert; then
|
|
mechanism=external
|
|
fi
|
|
|
|
NS=sasl DEFAULT_N=$mechanism state_resolve mechanism
|
|
NS=sasl DEFAULT=yes state_resolve required
|
|
|
|
if ! NS=sasl state_has mechanism; then
|
|
die "please specify an appropriate sasl mechanism"
|
|
fi
|
|
|
|
advertised=$(NS=avail_caps state_get sasl)
|
|
mechanism=$(NS=sasl state_get mechanism)
|
|
|
|
if [[ ,$advertised, = *,"${mechanism^^}",* ]]; then
|
|
irc_authenticate mechanism "$(NS=sasl state_get mechanism)"
|
|
else
|
|
if NS=sasl QUIET="" state_get required; then
|
|
die "%s is not a valid sasl mechanism" "$mechanism"
|
|
else
|
|
log_warn "connecting without sasl"
|
|
irc_cap end
|
|
fi
|
|
fi
|
|
else
|
|
irc_cap end
|
|
fi
|
|
esac
|
|
fi
|
|
}
|
|
|
|
on_msg_AUTHENTICATE_enroll() {
|
|
# Any fail scenario is already covered.
|
|
# shellcheck disable=SC2155
|
|
local mechanism=$(NS=sasl state_get mechanism)
|
|
irc_authenticate +
|
|
}
|
|
|
|
on_sys_init_010_bootup() {
|
|
DEFAULT=${USER:-uplime} state_resolve owner
|
|
DEFAULT=\` state_resolve trigger
|
|
DEFAULT=yes state_resolve dev
|
|
}
|
|
|
|
on_sys_init_020_welcome() {
|
|
NS=irc state_resolve chans
|
|
}
|
|
|
|
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"
|
|
NS=isupport state_put "$key" "$value"
|
|
log_trace "isupport: %s = %s" "$key" "$value"
|
|
done
|
|
}
|
|
|
|
on_sys_register_welcome() {
|
|
if NS=irc state_has chans; then
|
|
irc_join "$(NS=irc state_get chans)"
|
|
fi
|
|
}
|
|
|
|
###
|
|
# magic required to make privmsg work
|
|
###
|
|
|
|
on_sys_register_privmagic() {
|
|
NS=irc state_put nick "${msg_args[0]}"
|
|
}
|
|
|
|
on_msg_354_privmagic() {
|
|
if (( msg_args[1] == 42 )); then
|
|
log_debug "received the identifying who"
|
|
NS=irc state_put ident "${msg_args[2]}"
|
|
NS=irc state_put host "${msg_args[3]}"
|
|
fi
|
|
}
|
|
|
|
on_msg_396_privmagic() {
|
|
NS=irc state_put host "${msg_args[1]}"
|
|
}
|
|
|
|
###
|
|
# irc receive handlers
|
|
###
|
|
|
|
irc_on_AUTHENTICATE() {
|
|
log_debug "received authentication acknowledgement"
|
|
}
|
|
|
|
irc_on_CAP() {
|
|
local cap caps
|
|
|
|
case ${msg_args[1]^^} in
|
|
LS)
|
|
caps=${msg_args[-1]}
|
|
|
|
while [[ $caps ]]; do
|
|
cap=${caps%% *} caps=${caps#"$cap"} caps=${caps# }
|
|
|
|
if [[ $cap = *=* ]]; then
|
|
NS=avail_caps state_put "${cap%%=*}" "${cap#*=}"
|
|
else
|
|
NS=avail_caps state_put "$cap" ""
|
|
fi
|
|
done
|
|
;;
|
|
NAK)
|
|
log_error "unable to request capabilities for %s" "$(NS=irc state_get nick)"
|
|
;;
|
|
ACK)
|
|
log_debug "have successfully received capabilities from server: %s" "${msg_args[-1]}"
|
|
esac
|
|
}
|
|
|
|
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_authenticate() {
|
|
case ${1,,} in
|
|
mechanism)
|
|
net_send "AUTHENTICATE %s" "${2^^}"
|
|
;;
|
|
*)
|
|
net_send "AUTHENTICATE %s" "$*"
|
|
esac
|
|
}
|
|
|
|
irc_cap() {
|
|
case ${1,,} in
|
|
ls)
|
|
net_send "CAP LS %s" "${2-302}"
|
|
;;
|
|
req)
|
|
shift
|
|
net_send "CAP REQ :%s" "$*"
|
|
;;
|
|
end)
|
|
net_send "CAP END"
|
|
esac
|
|
}
|
|
|
|
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_pass() {
|
|
net_send "PASS $1"
|
|
}
|
|
|
|
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_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_is_good_variable() {
|
|
[[ $1 =~ ^[A-Za-z_][A-Za-z0-9_]+$ ]]
|
|
}
|
|
|
|
plugin_reg() {
|
|
if ! plugin_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 plugin_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_sys_exit_
|
|
run_callbacks on_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
|