1198 lines
23 KiB
Bash
Executable file
1198 lines
23 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
###
|
|
# stats
|
|
###
|
|
|
|
if [[ ! -v START_TIME ]]; then
|
|
export START_TIME
|
|
printf -v START_TIME '%(%s)T' -1
|
|
fi
|
|
|
|
if [[ -v RELOADED ]]; then
|
|
(( RELOADED += 1 ))
|
|
else
|
|
export RELOADED=0
|
|
fi
|
|
|
|
export LAST_RELOAD
|
|
printf -v LAST_RELOAD '%(%s)T' -1
|
|
|
|
if [[ ! -v LONGEST_LIFE ]]; then
|
|
export LONGEST_LIFE=0
|
|
elif (( LONGEST_LIFE < SECONDS )); then
|
|
export LONGEST_LIFE=$SECONDS
|
|
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+=", "
|
|
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 > LONGEST_LIFE )); then
|
|
export 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://ahti.space/git/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 > LONGEST_LIFE )); then
|
|
export 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"
|
|
;;
|
|
stats)
|
|
privmsg "$to" "running since $(printf '%(%c)T' "$START_TIME")"
|
|
privmsg "$to" "last reload at $(printf '%(%c)T' "$LAST_RELOAD")"
|
|
|
|
if (( LONGEST_LIFE > SECONDS )); then
|
|
privmsg "$to" "longest life so far is $(seconds "$LONGEST_LIFE")"
|
|
else
|
|
privmsg "$to" "longest life so far is $(seconds "$SECONDS") (this one)"
|
|
fi
|
|
|
|
privmsg "$to" "reloaded $RELOADED times"
|
|
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
|
|
}
|
|
|
|
###
|
|
# 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
|