summaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/channels.bash71
-rw-r--r--modules/ctcpversion.bash32
-rw-r--r--modules/example.bash69
-rw-r--r--modules/ibip.bash41
-rw-r--r--modules/invitejoin.bash15
-rw-r--r--modules/log.bash44
-rw-r--r--modules/ping.bash17
-rw-r--r--modules/sed.bash189
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
+}