#!/usr/bin/env bash ### # 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" fi } debug() { LEVEL=debug log "$@" } info() { LEVEL=info log "$@" } warn() { LEVEL=warn log "$@" } error() { LEVEL=error log "$@" } ### # argument parser for parsing arguments ## 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 ### # default config ## server=${opts[server]:-irc.libera.chat} tls=${opts[tls]:-no} if [[ $tls = yes ]]; then if ! hash socat 2>/dev/null; then printf 'please install socat to use tls with rowbot.\n' >&2 exit 1 fi port=${opts[port]:-6697} else port=${opts[port]:-6667} fi level=${opts[log-level]:-info} if [[ ${opts[log]} ]]; then exec {log}>"${opts[log]}" else log=1 fi nick=${opts[nick]:-rowbot-dev} ident=${opts[ident]:-rowbot} realname=${opts[realname]:-rowbot} chan=${opts[chan]:-} trigger=${opts[trigger]:-\`} fact_root=${opts[fact-root]:-.} ### # net code ### if [[ $tls = yes ]]; then coproc sock { socat OPENSSL:"$server":"$port" -; } exec {in_sock}<&"${sock[0]}" {out_sock}>&"${sock[1]}" else exec {sock}<>/dev/tcp/"$server"/"$port" in_sock=$sock out_sock=$sock fi send() { local fmt 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 "${params[0]}" exit } on_JOIN() { info "%s has joined %s" "$from" "${params[0]}" } on_MODE() { if (( ${#params[@]} == 2 )); then info "%s sets mode(s) %s on %s" "$from" "${params[1]}" "${params[0]}" else warn "mode line was not handled: %s" "$orig_line" fi } on_NOTICE() { info "[%s/%s] %s" "$from" "${params[0]}" "${params[1]}" } on_PING() { pong "${params[1]}" debug "received ping: %s" "${params[0]}" } on_PONG() { debug "received pong: %s" "${params[1]}" } on_QUIT() { info "%s has disconnected: %s" "$from" "${params[0]}" } on_001() { info %s "${params[1]}" if [[ $chan ]]; then join "$chan" fi while true; do ping "row your bot gently down the stream" sleep 10 done & trap "kill $!" EXIT } on_002() { info %s "${params[1]}" } on_003() { info %s "${params[1]}" } on_004() { debug "%s " "${params[@]:1}" } declare -A isupport on_005() { local param key value for param in "${params[@]:1:${#params[@]}-2}"; do IFS== read -r key value <<< "$param" isupport[$key]=$value debug "isupport: %s = %s" "$key" "$value" done } on_250() { info %s "${params[1]}" } on_251() { info %s "${params[1]}" } on_252() { info "There are %d operators online" "${params[1]}" } on_253() { info "There are %d unknown connections" "${params[1]}" } on_254() { info "There are %d channels formed" "${params[1]}" } on_255() { info %s "${params[1]}" } on_265() { info %s "${params[3]}" } on_266() { info %s "${params[3]}" } on_372() { info %s "${params[1]}" } on_375() { debug %s "${params[1]}" } on_376() { debug %s "${params[1]}" } ### # irc send code ### join() { send "JOIN %s" "$1" } nick() { send "NICK %s" "$1" info "changing nickname to %s" "$1" } ping() { send "PING :%s" "$1" } pong() { send "PONG %s" "$1" } privmsg() { send "PRIVMSG %s :\u200b%s" "$1" "$2" } user() { send "USER %s * * :%s" "$ident" "$realname" } ### # app hooks ## hook_PRIVMSG_factoids() { if [[ ${params[0]:0:1} != \# ]]; then return 0 elif [[ ${words[0]} = "$trigger"* ]]; then case ${words[0]:${#trigger}} in is) if (( ${#words[@]} < 3 )); then return 0 fi local key val key=${params[1]#*"$trigger"is} key=${key# } val=${key#* } key=${key%% *} info "%s said in %s to remember %s as %s" "$from" "${params[0]}" "$key" "$val" privmsg "${params[0]}" "I'm sure I'll remember that." mkdir -p "$fact_root"/"${params[0]}" printf %s "$val" > "$fact_root"/"${params[0]}"/"$key" ;; isnt) if (( ${#words[@]} < 2 )); then return 0 fi local key key=${params[1]#*"$trigger"isnt} key=${key# } if [[ -f $fact_root/${params[0]}/$key ]]; then info "%s said in %s to delete %s" "$from" "${params[0]}" "$key" privmsg "${params[0]}" "I forgot what that was anyways." rm -f "$fact_root"/"${params[0]}"/"$key" fi ;; ls) local facts=( "$fact_root"/"${params[0]}"/* ) privmsg "${params[0]}" "${facts[*]##*/}" ;; *) local key=${params[1]:1} if [[ -f $fact_root/${params[0]}/$key ]]; then privmsg "${params[0]}" "$from: $(<"$fact_root"/"${params[0]}"/"$key")" fi esac fi } ### # driver ### nick "$nick" user "$ident" "$realname" while recv line; do params=( ) has_words=no 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 params+=("${line:1}") line="" has_words=yes else param=${line%% *} params+=("$param") line=${line#"$param"} line=${line# } fi done if [[ $has_words = yes ]]; then read -ra words <<< "${params[@]:(-1)}" else words=( ) fi while IFS= read -r hook; do "$hook" done < <(compgen -A function "hook_${cmd^^}_") if hash "on_${cmd^^}" 2>/dev/null; then "on_${cmd^^}" else warn "unhandled line: %s" "$orig_line" fi done