#!/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 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