diff options
Diffstat (limited to 'modules')
-rw-r--r-- | modules/channels.bash | 71 | ||||
-rw-r--r-- | modules/ctcpversion.bash | 32 | ||||
-rw-r--r-- | modules/example.bash | 69 | ||||
-rw-r--r-- | modules/ibip.bash | 41 | ||||
-rw-r--r-- | modules/invitejoin.bash | 15 | ||||
-rw-r--r-- | modules/log.bash | 44 | ||||
-rw-r--r-- | modules/ping.bash | 17 | ||||
-rw-r--r-- | modules/sed.bash | 189 |
8 files changed, 478 insertions, 0 deletions
diff --git a/modules/channels.bash b/modules/channels.bash new file mode 100644 index 0000000..04c8a3b --- /dev/null +++ b/modules/channels.bash @@ -0,0 +1,71 @@ +# modules/channels.bash +# +# Provides persistant storage of channels the bot is in. +# +# Settings: +# CHANNELS_LIST: file to store channel list in + + +if [ -z "$IRCBOT_MODULE" ]; then + printf "error: %s is a module for ircbot.bash and should not be run separately\n" "$0" + exit 1 +fi + + +channels_list=() + + +# joins channels in the list after connecting +on_connect() { # no args + local ch + channels_load + for ch in "${channels_list[@]}"; do + sendmsg JOIN "$ch" + done +} + +# adds a joined channel to the list +on_self_join() { # args: $1 - channel + channels_add "$1" +} + +# removes a channel from the list +on_self_kick() { # args: $1 - source, $2 - channel, $3 - kick message + channels_remove "$2" +} + +# removes a channel from the list +on_self_part() { # args: $1 - channel, $2 - part message + channels_remove "$1" +} + + +channels_load() { # no args + local line + channels_list=() + if [ -f "$CHANNELS_LIST" ]; then + while IFS= read -r line || [ -n "$line" ]; do + verbose 'loaded channel %s' "$line" + channels_list+=("$line") + done < "$CHANNELS_LIST" + fi +} + +channels_add() { # args: $1 - channel + channels_load + verbose 'adding channel %s' "$1" + channels_list+=("$1") + channels_dump +} + +channels_remove() { # args: $1 - channel + channels_load + verbose 'removing channel %s' "$1" + printf "%s\n" "${channels_list[@]}" | grep -xvF "$1" > "$CHANNELS_LIST" # XXX: ugly hack + channels_load +} + +channels_dump() { + verbose 'dumping channel list: %s' "${channels_list[*]}" + printf "%s\n" "${channels_list[@]}" | sort | uniq > "$CHANNELS_LIST" +} diff --git a/modules/ctcpversion.bash b/modules/ctcpversion.bash new file mode 100644 index 0000000..092013b --- /dev/null +++ b/modules/ctcpversion.bash @@ -0,0 +1,32 @@ +# modules/ctcpversion.bash +# +# Responds to CTCP VERSION direct message requests. +# +# Settings: +# CTCP_VERSION: response to the CTCP VERSION request; optional, defaults to bash version + + +if [ -z "$IRCBOT_MODULE" ]; then + printf "error: %s is a module for ircbot.bash and should not be run separately\n" "$0" + exit 1 +fi + + +# respond to CTCP VERSION requests +on_ctcp() { # args: $1 - source, $2 - CTCP command, $3 - CTCP argument + local version interpreter + if [[ $2 == VERSION ]]; then + if [[ -n ${CTCP_VERSION:-} ]]; then + version="$CTCP_VERSION" + else + interpreter=sh # I should make it actually find out which interpreter it's running if it's not bash or zsh; also, fails on dash as it lacks --version + if [ "$BASH_VERSION" ]; then + interpreter=bash + elif [ "$ZSH_VERSION" ]; then + interpreter=zsh + fi + version="$("$interpreter" --version 2>&1 | sed 1q)" + fi + sendmsg NOTICE "$(parse_source_nick "$1")" "$(sed 's/^.*$/\x01&\x01/' <<< "VERSION $version")" + fi +} diff --git a/modules/example.bash b/modules/example.bash new file mode 100644 index 0000000..1893722 --- /dev/null +++ b/modules/example.bash @@ -0,0 +1,69 @@ +# modules/example.bash +# +# Provides example handlers for an ircbot.bash module. +# This file is sourced by ircbot.bash when the module is loaded. +# Any handlers may be omitted in modules if they're not used. +# If an event matches multiple handlers, they're all called (e.g. on_readmsg is +# always called alongside on_privmsg). +# +# Global variables: +# +# IRCBOT_HOST: IRC server address +# IRCBOT_PORT: IRC server port +# IRCBOT_NICK: bot nick +# IRCBOT_LOGIN: bot login name +# IRCBOT_REALNAME: bot realname +# IRCBOT_MODULE: current module the bot is running + + +if [ -z "$IRCBOT_MODULE" ]; then + printf "error: %s is a module for ircbot.bash and should not be run separately\n" "$0" + exit 1 +fi + + +# on_connect is called when the bot connects (or reconnects) to the server +on_connect() { # no args +} + +# on_disconnect is called when the bot disconnects from the server +on_disconnect() { # no args +} + +# on_readmsg is called whenever a message is read from the IRC server +on_readmsg() { # args: $1 - raw message, $2 - source, $3 - command, $4... - args +} + +# on_sendmsg is called whenever sendmsg is used to send IRC messages +# If it returns non-zero, the sendmsg itself is aborted. +# Be careful not to create infinite sendmsg/on_sendmsg loops. +on_sendmsg() { # args: $1 - raw message, $2 - command, $3... - args +} + +# on_privmsg is called whenever a PRIVMSG is received +on_privmsg() { # args: $1 - source, $2 - channel/target, $3 - message +} + +# on_dm is called whenever a PRIVMSG direct (not channel) message is received +on_dm() { # args: $1 - source, $2 - message +} + +# on_ctcp is called whenever a CTCP PRIVMSG direct (not channel) message is received +on_ctcp() { # args: $1 - source, $2 - CTCP command, $3 - CTCP argument +} + +# on_self_join is called whenever the bot successfully joins a channel +on_self_join() { # args: $1 - channel +} + +# on_self_kick is called whnever the bot is kicked from a channel +on_self_kick() { # args: $1 - source, $2 - channel, $3 - kick message +} + +# on_self_part is called whenever the bot parts a channel +on_self_part() { # args: $1 - channel, $2 - part message +} + +# on_self_invite is called whenever the bot is invited into a channel +on_self_invite() { # args: $1 - source, $2 - channel +} diff --git a/modules/ibip.bash b/modules/ibip.bash new file mode 100644 index 0000000..13efd0c --- /dev/null +++ b/modules/ibip.bash @@ -0,0 +1,41 @@ +# modules/ibip.bash +# +# Responds to ``.bots'' +# +# Settings: +# IBIP_TIMEOUT: seconds that must pass between IBIP responses; optional +# IBIP_COMMENT: the comment at the end of the IBIP message; optional +# IBIP_NOTICE: set to 1 to respond with NOTICE instead of PRIVMSG; optional + + +if [ -z "$IRCBOT_MODULE" ]; then + printf "error: %s is a module for ircbot.bash and should not be run separately\n" "$0" + exit 1 +fi + + +ibip_last=0 + +if [[ -z ${IBIP_COMMENT:-} ]]; then + IBIP_COMMENT='See https://git.clsr.net/mbibot' +fi + +# on_privmsg is called whenever a PRIVMSG is received +on_privmsg() { # args: $1 - source, $2 - channel/target, $3 - message + local ts where msgtype + where="$2" + if [[ $where == "$IRCBOT_NICK" ]]; then + where="$(parse_source_nick "$1")" + fi + msgtype=PRIVMSG + if [[ ${IBIP_NOTICE:-0} -ne 0 ]]; then + msgtype=NOTICE + fi + if [[ $3 == ".bots" ]]; then + ts="$(printf '%(%s)T' -1)" + if [[ -z ${IBIP_TIMEOUT:-} ]] || ((ts - ibip_last > IBIP_TIMEOUT)); then + ibip_last="$ts" + sendmsg "$msgtype" "$where" "Reporting in! [bash] $IBIP_COMMENT" + fi + fi +} diff --git a/modules/invitejoin.bash b/modules/invitejoin.bash new file mode 100644 index 0000000..d76d85d --- /dev/null +++ b/modules/invitejoin.bash @@ -0,0 +1,15 @@ +# modules/invitejoin.bash +# +# Joins channels when invited. + + +if [ -z "$IRCBOT_MODULE" ]; then + printf "error: %s is a module for ircbot.bash and should not be run separately\n" "$0" + exit 1 +fi + + +# joins the channel the bot was invited to +on_self_invite() { # args: $1 - source, $2 - channel + sendmsg JOIN "$2" +} diff --git a/modules/log.bash b/modules/log.bash new file mode 100644 index 0000000..e9fc665 --- /dev/null +++ b/modules/log.bash @@ -0,0 +1,44 @@ +# modules/log.bash +# +# Logs IRC messages and bot debug/error output. +# +# Settings: +# LOG_IRC: log file for IRC protocol traffic +# LOG_ERR: log file for stderr messages +# LOG_SHOW_IRC: set to non-zero to show IRC output on stdout + + +if [ -z "$IRCBOT_MODULE" ]; then + printf "error: %s is a module for ircbot.bash and should not be run separately\n" "$0" + exit 1 +fi + + +if [[ -n ${LOG_IRC:-} ]]; then + if [[ ${LOG_SHOW_IRC:-0} -ne 0 ]]; then + exec 14> >(tee -a -- "$LOG_IRC") + else + exec 14>>"$LOG_IRC" + fi +elif [[ ${LOG_SHOW_IRC:-0} -ne 0 ]]; then + exec 14>&1 +fi + +if [[ -n ${LOG_ERR:-} ]]; then + exec 2> >(tee -a -- "$LOG_ERR" >&2) +fi + + +# log received messages +on_readmsg() { # args: $1 - raw message, $2 - source, $3 - command, $4... - args + printf "%s <<< %s\n" "$(log_timestamp)" "$1" >&14 +} + +# log sent messages +on_sendmsg() { # args: $1 - raw message, $2 - command, $3... - args + printf "%s >>> %s\n" "$(log_timestamp)" "$1" >&14 +} + +log_timestamp() { + TZ=UTC date -u +%s.%N +} diff --git a/modules/ping.bash b/modules/ping.bash new file mode 100644 index 0000000..ece6a1f --- /dev/null +++ b/modules/ping.bash @@ -0,0 +1,17 @@ +# modules/ping.bash +# +# Responds to IRC PINGs. + + +if [ -z "$IRCBOT_MODULE" ]; then + printf "error: %s is a module for ircbot.bash and should not be run separately\n" "$0" + exit 1 +fi + + +# respond to PINGs +on_readmsg() { # args: $1 - raw message, $2 - source, $3 - command, $4... - args + if [[ $3 == PING ]]; then + sendmsg PONG "${@:4}" + fi +} diff --git a/modules/sed.bash b/modules/sed.bash new file mode 100644 index 0000000..b75a252 --- /dev/null +++ b/modules/sed.bash @@ -0,0 +1,189 @@ +# modules/sed.bash +# +# Provides sedbot. +# +# Settings: +# SED_TIMEOUT_BIN: path to the timeout script¹; optional to prevent regex DoS +# SED_TIMEOUT_MEM: maximum amount of memory (in kilobytes) a sed process may use +# +# ¹: https://github.com/pshved/timeout + + +if [ -z "$IRCBOT_MODULE" ]; then + printf "error: %s is a module for ircbot.bash and should not be run separately\n" "$0" + exit 1 +fi + +declare -g -A sed_messages + +# seds a previous message if instucted to +on_privmsg() { # args: $1 - source, $2 - channel/target, $3 - message + local where nick msg + local targetednick origmsg origkey + local key target fromnick regexed ctcp + + where="$2" + nick="$(parse_source_nick "$1")" + msg="$3" + if [[ $where == "$IRCBOT_NICK" ]]; then + where="$nick" + fi + + # targeted means the message was ``<user1> user2: s/foo/bar/'' (targets user2's last message) + origmsg="$msg" + origkey="$where $nick" + if targetednick="$(parse_targeted_nick "$msg")"; then + nick="$targetednick" + msg="$(parse_targeted_msg "$msg")" + fi + + + key="$where $nick" + target="${sed_messages[$key]:-}" + + # handle regexing CTCP ACTIONs properly + ctcp="$(parse_ctcp_command "$target")" || true + if [[ $ctcp == ACTION ]]; then + target="$(parse_ctcp_message "$target")" + fromnick="$(printf "\\x02* %s\\x02" "$nick")" + else + fromnick="<$nick>" + fi + + if regexed="$(sed_replace "$msg" "$target")"; then # if a replacement was done, send it + sendmsg PRIVMSG "$where" "$fromnick $regexed" + else # otherwise, store the triggering message + sed_messages[$origkey]="$origmsg" + fi +} + +sed_replace() { # args: $1 - sed s expression, $2 - text to regex + local msg target + local l pos i p del + local regexps ok t target + + msg="$1" + target="$2" + + del="${msg:1:1}" + if [[ -z $del ]] || [[ $(indexof '/|,!:' "$del") -lt 0 ]]; then + return 1 + fi + + # TODO: rewrite so that only one expression is tokenized at once (allows different delimiters) + + # tokenize + l=() + pos="$(indexof "$msg" "$del")" + while ((pos >= 0)); do + i=0 + ((p=pos-i-1)) + while ((p >= 0)) && [[ ${msg:$p:1} == '\' ]]; do # count \ characters + ((i++)) + ((p=pos-i-1)) + done + if ((i%2 == 0)); then + l+=("${msg:0:$pos}") + ((p=pos+1)) + msg="${msg:$p}" + pos=0 + else + ((pos++)) + fi + p="$(indexof "${msg:$pos}" "$del")" + if ((p >= 0)); then + ((pos+=p)) + else + pos=$p + fi + done + l+=("$msg") + + # l is now an array of the expr separated by unescaped delimiters + + i=0 + regexps=() + ok=1 + + # s/expr1/repl1/opts1 s/expr2/repl2/opts2 s/expr3/repl3 + while ((i < ${#l[@]})); do + # begins with s + if [ "${l[$i]}" != "s" ]; then + break + fi + ((i++)) + + # expr + if ((i >= ${#l[@]})); then + break + fi + exp="${l[$i]}" + ((i++)) + + # repl + if ((i >= ${#l[@]})); then + break + fi + repl="${l[$i]}" + ((i++)) + + # opts + opts='' + if ((i < ${#l[@]})); then + opts="${l[$i]}" + p=0 + while ((p < ${#opts})); do + c="${opts:$p:1}" + if ! [[ $c =~ ^[ig0-9]$ ]]; then # allowed opts are 0-9, i and g + ok=0 + break + fi + ((p++)) + done + if ! ((ok)); then + # multiple regexps per line + if [[ "${opts:$p:1}" == ' ' || "${opts:$p:1}" == ';' ]]; then # expression separators are space and ; + p1=$p + while ((p < ${#opts})); do + if [[ "${opts:$p:1}" != ' ' && "${opts:$p:1}" != ';' ]]; then + break + fi + p=$((p+1)) + done + l[$i]="${opts:$p}" + opts="${opts:0:$p1}" + ok=1 + else + break + fi + fi + fi + + if ((ok)); then + regexps+=("s$del$exp$del$repl$del$opts") + fi + done + + + if ! ((ok)) || ((${#regexps[@]} == 0)); then + return 1 + fi + + t="$target" + for re in "${regexps[@]}"; do + if [[ -n ${SED_TIMEOUT_BIN:-} ]]; then + target="$("$SED_TIMEOUT_BIN" -m "$SED_TIMEOUT_MEM" sed -e "$re" <<< "$target")" + else + target="$(sed -e "$re" <<< "$target")" + fi + done + target="$(trimrn <<< "$target")" + verbose "sed '%s' <<< '%s' >>> '%s'" "$1" "$2" "$target" + if [[ $target != "$t" ]]; then + if [[ -n $target ]]; then + trimrn <<< "$target" + return 0 + fi + fi + return 1 +} |