diff --git a/deb/DEBIAN/control b/deb/DEBIAN/control deleted file mode 100644 index 208a4a6..0000000 --- a/deb/DEBIAN/control +++ /dev/null @@ -1,9 +0,0 @@ -Package: rowbot -Version: 1.0-0 -Section: base -Priority: optional -Architecture: i386 -Depends: socat (>= 1.7.3) -Maintainer: Nick Chambers -Description: Rowbot - A smol bot for remembering things diff --git a/deb/usr/local/bin/rowbot b/deb/usr/local/bin/rowbot deleted file mode 100755 index ab91da6..0000000 --- a/deb/usr/local/bin/rowbot +++ /dev/null @@ -1,1197 +0,0 @@ -#!/usr/bin/env bash - -### -# lore -### - -export LORE_THIS_RELOAD -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 -elif (( LORE_LONGEST_LIFE < SECONDS )); then - export LORE_LONGEST_LIFE=$SECONDS -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://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 > 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) - declare -p LORE_LONGEST_LIFE - 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 - - privmsg "$to" "lived $LORE_LIVES 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 diff --git a/rowbot b/rowbot deleted file mode 120000 index ff5c180..0000000 --- a/rowbot +++ /dev/null @@ -1 +0,0 @@ -deb/usr/local/bin/rowbot \ No newline at end of file diff --git a/rowbot b/rowbot new file mode 100755 index 0000000..ab91da6 --- /dev/null +++ b/rowbot @@ -0,0 +1,1197 @@ +#!/usr/bin/env bash + +### +# lore +### + +export LORE_THIS_RELOAD +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 +elif (( LORE_LONGEST_LIFE < SECONDS )); then + export LORE_LONGEST_LIFE=$SECONDS +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://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 > 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) + declare -p LORE_LONGEST_LIFE + 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 + + privmsg "$to" "lived $LORE_LIVES 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