aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/access.sh99
-rw-r--r--lib/channels.sh125
-rw-r--r--lib/commands.sh265
-rw-r--r--lib/config.sh219
-rw-r--r--lib/debug.sh84
-rw-r--r--lib/feedback.sh59
-rw-r--r--lib/hash.sh312
-rw-r--r--lib/log.sh285
-rw-r--r--lib/main.sh566
-rw-r--r--lib/misc.sh267
-rw-r--r--lib/modules.sh447
-rw-r--r--lib/numerics.sh348
-rw-r--r--lib/parse.sh100
-rw-r--r--lib/send.sh178
-rw-r--r--lib/server.sh337
-rw-r--r--lib/time.sh110
16 files changed, 3801 insertions, 0 deletions
diff --git a/lib/access.sh b/lib/access.sh
new file mode 100644
index 0000000..28fa358
--- /dev/null
+++ b/lib/access.sh
@@ -0,0 +1,99 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Access control library.
+#---------------------------------------------------------------------
+
+
+#---------------------------------------------------------------------
+## Check for owner access.
+## @Type API
+## @param n!u@h mask
+## @return 0 If access was granted
+## @return 1 If access was denied.
+#---------------------------------------------------------------------
+access_check_owner() {
+ debug_log_caller "$@"
+ security_assert_argc 1 1 "$@" || {
+ log_error "Aiie! Access denied because of incorrect function call!"
+ return 1
+ }
+ local index
+ for index in ${!config_access_mask[*]}; do
+ if [[ "$1" =~ ${config_access_mask[$index]} ]] && list_contains "config_access_capab[$index]" 'owner'; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+#---------------------------------------------------------------------
+## Check for access in scope.
+## @Type API
+## @param Capability to check for.
+## @param n!u@h mask
+## @param What scope
+## @return 0 If access was granted
+## @return 1 If access was denied.
+#---------------------------------------------------------------------
+access_check_capab() {
+ debug_log_caller "$@"
+ security_assert_argc 3 3 "$@" || {
+ log_error "Aiie! Access denied because of incorrect function call!"
+ return 1
+ }
+ local index
+ for index in ${!config_access_mask[*]}; do
+ if [[ "$2" =~ ${config_access_mask[$index]} ]] && \
+ [[ "$3" =~ ${config_access_scope[$index]} ]]; then
+ if list_contains "config_access_capab[$index]" "$1" || \
+ list_contains "config_access_capab[$index]" "owner"; then
+ return 0
+ fi
+ fi
+ done
+ return 1
+}
+
+#---------------------------------------------------------------------
+## Used to log actions like "did a rehash" if access was granted.
+## @Type API
+## @param n!u@h mask
+## @param What happened.
+#---------------------------------------------------------------------
+access_log_action() {
+ log_info_file owner.log "$1 performed the restricted action: $2"
+}
+
+#---------------------------------------------------------------------
+## Return error message about failed access to someone, and log it
+## @Type API
+## @param n!u@h mask
+## @param What they tried to do
+## @param What capability they need
+#---------------------------------------------------------------------
+access_fail() {
+ log_error_file access.log "$1 tried to \"$2\" but lacks access."
+ local nick=
+ parse_hostmask_nick "$sender" 'nick'
+ send_notice "$nick" "Permission denied. You need the capability \"$3\" to do this action."
+}
diff --git a/lib/channels.sh b/lib/channels.sh
new file mode 100644
index 0000000..df38c55
--- /dev/null
+++ b/lib/channels.sh
@@ -0,0 +1,125 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Channel management.
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Space separated list of current channels
+## @Type API
+#---------------------------------------------------------------------
+channels_current=""
+
+#---------------------------------------------------------------------
+## Join a channel
+## @Type API
+## @param The channel to join.
+## @param Is a channel key, if any.
+#---------------------------------------------------------------------
+channels_join() {
+ local channel="$1"
+ local key=""
+ [[ -n "$2" ]] && key=" $2"
+ send_raw "JOIN ${channel}${key}"
+}
+
+#---------------------------------------------------------------------
+## Part a channel
+## @Type API
+## @param The channel to part
+## @param Is a reason.
+#---------------------------------------------------------------------
+channels_part() {
+ local channel="$1"
+ local reason=""
+ [[ -n "$2" ]] && reason=" :$2"
+ send_raw "PART ${channel}${reason}"
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Internal function!
+## Adds channels to the list
+## @Type Private
+## @param The channel to add
+#---------------------------------------------------------------------
+channels_add() {
+ channels_current+=" $1"
+}
+
+#---------------------------------------------------------------------
+## Internal function!
+## Removes channels to the list
+## @Type Private
+## @param The channel to remove
+#---------------------------------------------------------------------
+channels_remove() {
+ list_remove channels_current "$1" channels_current
+}
+
+#---------------------------------------------------------------------
+## Check if we parted, called from main loop
+## @Type Private
+## @param n!u@h mask
+## @param Channel parted.
+## @param Reason (ignored).
+#---------------------------------------------------------------------
+channels_handle_part() {
+ local whoparted=
+ parse_hostmask_nick "$1" 'whoparted'
+ if [[ $whoparted == $server_nick_current ]]; then
+ channels_remove "$2"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Check if we got kicked, called from main loop
+## @Type Private
+## @param n!u@h mask of kicker
+## @param Channel kicked from.
+## @param Nick of kicked user
+## @param Reason (ignored).
+#---------------------------------------------------------------------
+channels_handle_kick() {
+ local whogotkicked="$3"
+ if [[ $whogotkicked == $server_nick_current ]]; then
+ channels_remove "$2"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Check if we joined, called from main loop
+## @Type Private
+## @param n!u@h mask
+## @param Channel joined.
+#---------------------------------------------------------------------
+channels_handle_join() {
+ local whojoined=
+ parse_hostmask_nick "$1" 'whojoined'
+ if [[ $whojoined == $server_nick_current ]]; then
+ channels_add "$2"
+ fi
+}
diff --git a/lib/commands.sh b/lib/commands.sh
new file mode 100644
index 0000000..b914a58
--- /dev/null
+++ b/lib/commands.sh
@@ -0,0 +1,265 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Handle registering of commands
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## List of commands (maps to function for the command), a hash
+## @Note Dummy variable to document the fact that it is a hash.
+## @Type Private
+#---------------------------------------------------------------------
+commands_list=''
+
+#---------------------------------------------------------------------
+## List of functions (by module), a hash
+## @Note Dummy variable to document the fact that it is a hash.
+## @Type Private
+#---------------------------------------------------------------------
+commands_modules_functions=''
+
+#---------------------------------------------------------------------
+## List of commands (by function), a hash
+## @Note Dummy variable to document the fact that it is a hash.
+## @Type Private
+#---------------------------------------------------------------------
+commands_function_commands=''
+
+#---------------------------------------------------------------------
+## List of commands (by module)
+## @Note Dummy variable to document the fact that it is a hash.
+## @Type Private
+#---------------------------------------------------------------------
+commands_module_commands=''
+
+#---------------------------------------------------------------------
+## List of modules (by command)
+## @Note Dummy variable to document the fact that it is a hash.
+## @Type Private
+#---------------------------------------------------------------------
+commands_commands_module=''
+
+#---------------------------------------------------------------------
+## Comma separated list of all commands
+## @Type Semi-private
+#---------------------------------------------------------------------
+commands_commands=''
+
+# Just unset dummy variables.
+unset commands_list commands_modules_functions commands_function_commands commands_module_commands commands_module_commands
+
+#---------------------------------------------------------------------
+## Register a command.
+## @Type API
+## @param Module name
+## @param Function name (Part after module_modulename_handler_)
+## @param Command name (on IRC, may contain spaces) (optional, defaults to same as function name, that is $2)
+## @return 0 If successful
+## @return 1 If failed for other reason
+## @return 2 If invalid command name
+## @return 3 If the command already exists (maybe from some other module)
+## @return 4 If the function already exists for other command.
+## @return 5 If the function in question is not declared.
+#---------------------------------------------------------------------
+commands_register() {
+ # Speed isn't that important here, it is only called at module load after all.
+ local module="$1"
+ local function_name="$2"
+ local command_name="$3"
+ # Command name is optional
+ if [[ -z $command_name ]]; then
+ command_name="$function_name"
+ fi
+ # Check for valid command name
+ if ! [[ $command_name =~ ^[a-zA-Z0-9] ]]; then
+ log_error "commands_register_command: Module \"$module\" gave invalid command name \"$command_name\". First char of command must be alphanumeric."
+ return 2
+ fi
+ if ! [[ $command_name =~ ^[a-zA-Z0-9][^\ ,]*( [^, ]+)?$ ]]; then
+ log_error "commands_register_command: Module \"$module\" gave invalid command name \"$command_name\". A command can be at most 2 words and should have no trailing white space and may not contain a \",\" (comma)."
+ return 2
+ fi
+ # Bail out if command is already registered.
+ if hash_exists 'commands_list' "$command_name"; then
+ log_error "commands_register_command: Failed to register command from \"$module\": a command with the name \"$command_name\" already exists."
+ return 3
+ fi
+ # Bail out if the function already is mapped to some other command
+ if hash_exists 'commands_function_commands' "$function_name"; then
+ log_error "commands_register_command: Failed to register command from \"$module\": the function is already registered under another command name."
+ return 4
+ fi
+
+ # Does the function itself exist?
+ local full_function_name="module_${module}_handler_${function_name}"
+ if ! declare -F | grep -qe "^declare -f ${full_function_name}$"; then
+ log_error "commands_register_command: Failed to register command from \"$module\": the function $full_function_name does not exist"
+ return 5
+ fi
+ # So it was valid. Lets add it then.
+
+ # Store in module -> function mapping.
+ hash_append 'commands_modules_functions' "$module" "$function_name" || {
+ log_error "commands_register_command: module -> commands mapping failed: mod=\"$module\" func=\"$function_name\"."
+ return 1
+ }
+ # Store in command -> function mapping
+ hash_set 'commands_list' "$command_name" "$full_function_name" || {
+ log_error "commands_register_command: command -> function mapping failed: cmd=\"$command_name\" full_func=\"$full_function_name\"."
+ return 1
+ }
+ # Store in function -> command mapping
+ hash_set 'commands_function_commands' "$function_name" "$command_name" || {
+ log_error "commands_register_command: function -> command mapping failed: func=\"$function_name\" cmd=\"$command_name\"."
+ return 1
+ }
+ # Store in command -> module mapping
+ hash_set 'commands_commands_module' "$command_name" "$module" || {
+ log_error "commands_register_command: command -> module mapping failed: cmd=\"$command_name\" mod=\"$module\"."
+ return 1
+ }
+ # Store in module -> commands mapping (ick!)
+ hash_append 'commands_module_commands' "$module" "$command_name" ',' || {
+ log_error "commands_register_command: module -> command mapping failed: mod=\"$module\" cmd=\"$command_name\"."
+ }
+ # Store in comma-separated command list
+ if [[ $commands_commands ]]; then
+ commands_commands+=",$command_name" || return 1
+ else
+ commands_commands="$command_name" || return 1
+ fi
+}
+
+#---------------------------------------------------------------------
+## Get what module provides a command.
+## @param Command to find.
+## @param Variable to return in
+## @Type Semi-private
+#---------------------------------------------------------------------
+commands_provides() {
+ hash_get "commands_commands_module" "$1" "$2"
+}
+
+#---------------------------------------------------------------------
+## Get what commands exist in a module.
+## @param Command to find.
+## @param Variable to return comma separated list in
+## @Type Semi-private
+#---------------------------------------------------------------------
+commands_in_module() {
+ hash_get "commands_module_commands" "$1" "$2"
+}
+
+#---------------------------------------------------------------------
+## Will remove all commands from a module and unset the functions in question.
+## @Type Private
+## @param Module
+## @return 0 If successful (or no commands exist for module)
+## @return 1 If error
+## @return 2 If fatal error
+#---------------------------------------------------------------------
+commands_unregister() {
+ local module="$1"
+ # Are there any commands for the module?
+ hash_exists 'commands_modules_functions' "$module" || {
+ return 0
+ }
+ local function_name full_function_name command_name functions
+ # Get list of functions
+ hash_get 'commands_modules_functions' "$module" 'functions' || return 2
+ # Iterate through the functions
+ for function_name in $functions; do
+ # Get command name
+ hash_get 'commands_function_commands' "$function_name" 'command_name' || return 2
+ # Unset from function -> command hash
+ hash_unset 'commands_function_commands' "$function_name" || return 2
+ # Unset from command -> function hash
+ hash_unset 'commands_list' "$command_name" || return 2
+ # Unset from command -> module mapping
+ hash_unset 'commands_commands_module' "$command_name" || return 2
+ # Remove from command list.
+ list_remove 'commands_commands' "$command_name" 'commands_commands' "," || return 1
+ # Unset help strings (if any):
+ unset helpentry_${module}_${function_name}_syntax
+ unset helpentry_${module}_${function_name}_description
+ # Unset function itself.
+ full_function_name="module_${module}_handler_${function_name}"
+ unset "$full_function_name" || return 2
+ done
+ # Unset the module -> commands mapping.
+ hash_unset 'commands_module_commands' "$module" || return 2
+ # Finally unset module -> functions mapping.
+ hash_unset 'commands_modules_functions' "$module" || return 2
+}
+
+#---------------------------------------------------------------------
+## Process a line finding what command it would be
+## @Type Private
+## @param Sender
+## @param Target
+## @param Query
+## @return 0 If not a command
+## @return 1 If it indeed was a command that we therefore handled.
+## @return 2 A command but that didn't exist.
+#---------------------------------------------------------------------
+commands_call_command() {
+ local regex="${config_commands_listenregex}"
+ # Not on a channel?
+ if [[ ! $2 =~ ^# ]]; then
+ # Should we treat it as a command anyway?
+ if [[ $config_commands_private_always == 1 ]]; then
+ local regex="(${config_commands_listenregex})?"
+ fi
+ fi
+ # Check if it is a command.
+ # (${config_commands_listenregex}, followed by an alphanumeric char.)
+ if [[ "$3" =~ ^${regex}([a-zA-Z0-9].*) ]]; then
+ local data="${BASH_REMATCH[@]: -1}"
+ # Right, get the parts of the command
+ if [[ $data =~ ^([a-zA-Z0-9][^ ]*)( [^, ]+)?( .*)? ]]; then
+ local firstword="${BASH_REMATCH[1]}"
+ local secondword="${BASH_REMATCH[2]}"
+ local parameters="${BASH_REMATCH[3]}"
+
+ local function=
+ # Check for two word commands first.
+ hash_get 'commands_list' "${firstword}${secondword}" 'function'
+ if [[ -z "$function" ]]; then
+ # Maybe one word then?
+ hash_get 'commands_list' "$firstword" 'function'
+ if [[ "$function" ]]; then
+ parameters="${secondword}${parameters}"
+ # No, not that either
+ else
+ return 2
+ fi
+ fi
+
+ # So we got a command, now lets run it
+ # (strip leading white spaces) from parameters.
+ "$function" "$1" "$2" "${parameters## }"
+ return 1
+ fi
+ return 2
+ fi
+ return 0
+}
diff --git a/lib/config.sh b/lib/config.sh
new file mode 100644
index 0000000..0b344e7
--- /dev/null
+++ b/lib/config.sh
@@ -0,0 +1,219 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Configuration management
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Rehash config file.
+## @Type API
+## @return 0 Success.
+## @return 2 Not same config version.
+## @return 3 Failed to source. The bot should not be in an undefined state.
+## @return 4 Config validation on faked source failed. The bot should not be in an undefined state.
+## @return 5 Failed to source. The bot may be in an undefined state.
+## @Note If config validation fails at REAL source, the bot may quit. However this should never happen.
+#---------------------------------------------------------------------
+config_rehash() {
+ local new_conf_ver="$(grep -E '^config_version=' "$config_file")"
+ if ! [[ $new_conf_ver =~ ^config_version=$config_current_version ]]; then
+ log_error "REHASH: Not same config version. Rehash aborted."
+ return 2
+ fi
+ # Try sourceing in a subshell first to catch errors
+ # without causing bot to break
+ ( source "$config_file" )
+ if [[ $? -ne 0 ]]; then
+ log_error "REHASH: Failed faked source. Rehash aborted. (TIP: Check for syntax errors in config and any message above this message.)"
+ return 3
+ fi
+ # HACK: Subshell, then unset all but two config_ variables (one is readonly, the other is needed to validate)
+ # Then source config file and run validation on it.
+ ( unset -v $(sed 's/ *config_current_version */ /g;s/ *config_file */ /g' <<<"${!config_*}")
+ source "$config_file"
+ config_validate && config_validate_transport )
+ if [[ $? -ne 0 ]]; then
+ log_error "REHASH: Failed config validation on new config. Rehash aborted."
+ return 4
+ fi
+ # Source for real if that worked
+ source "$config_file"
+ if [[ $? -ne 0 ]]; then
+ log_error "REHASH: Failed real source. BOT MAY BE IN UNDEFINED STATE."
+ return 5
+ fi
+ # Lets force command line -v, it may have been overwritten by config.
+ if [[ $force_verbose -eq 1 ]]; then
+ config_log_stdout='1'
+ fi
+ local status
+ modules_load_from_config
+ for module in $modules_loaded; do
+ module_${module}_REHASH
+ status=$?
+ if [[ $status -eq 1 ]]; then
+ log_error "Rehash of ${module} failed, trying to unload it."
+ modules_unload "${module}" || {
+ log_fatal "Unloading of ${module} after failed rehash failed."
+ bot_quit "Fatal error in unload of module that failed to rehash"
+ }
+ fi
+ if [[ $status -eq 2 ]]; then
+ log_fatal "Rehash of ${module} failed in a FATAL way. Quitting"
+ bot_quit "Fatal error in rehash of module"
+ fi
+ done
+ log_info_stdout "Rehash successful"
+}
+
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## This will call logging if logging is setup,
+## otherwise just print to STDOUT, with prefix
+## @Type Private
+#---------------------------------------------------------------------
+config_dolog_fatal() {
+ if [[ $log_file ]]; then
+ log_fatal "$1"
+ else
+ echo "FATAL ERROR: $1"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Returns an error if the variable in question is empty/not set
+## @Note Works only for non-array variables
+## @Type Private
+## @param Variable name
+## @param Extra error line(s) to append (optional, one parameter for each extra line)
+#---------------------------------------------------------------------
+config_validate_check_exists() {
+ if [[ -z "${!1}" ]]; then
+ config_dolog_fatal "YOU MUST SET $1 IN THE CONFIG"
+ shift
+ # Do the rest of the messages
+ local line=
+ for line in "$@"; do
+ config_dolog_fatal "$line"
+ done
+ envbot_quit 2
+ fi
+}
+
+
+#---------------------------------------------------------------------
+## Validate config file
+## @Type Private
+#---------------------------------------------------------------------
+config_validate() {
+ # Note: normal logging is not initialized yet at this point,
+ # so we use config_dolog_fatal, that calls normal logging in case
+ # logging is loaded (like rehash).
+
+ # General settings
+ config_validate_check_exists config_firstnick
+ config_validate_check_exists config_ident
+ config_validate_check_exists config_gecos
+
+ # Server settings
+ config_validate_check_exists config_server
+ config_validate_check_exists config_server_port
+ config_validate_check_exists config_server_ssl
+
+ # Logging
+ config_validate_check_exists config_log_dir
+ config_validate_check_exists config_log_stdout
+ config_validate_check_exists config_log_raw
+ config_validate_check_exists config_log_colors
+
+ # Commands
+ config_validate_check_exists config_commands_listenregex
+ config_validate_check_exists config_commands_private_always
+
+ # Feedback
+ config_validate_check_exists config_feedback_unknown_commands
+
+ # Access
+ if [[ -z "${config_access_mask[1]}" ]]; then
+ config_dolog_fatal "YOU MUST SET AT LEAST ONE OWNER IN EXAMPLE CONFIG"
+ config_dolog_fatal "AND THAT OWNER MUST BE THE FIRST ONE (config_access_mask[1] that is)."
+ envbot_quit 1
+ fi
+ if ! list_contains "config_access_capab[1]" "owner"; then
+ config_dolog_fatal "YOU MUST SET AT LEAST ONE OWNER IN EXAMPLE CONFIG"
+ config_dolog_fatal "AND THAT OWNER MUST BE THE FIRST ONE (config_access_capab[1] that is)."
+ envbot_quit 1
+ fi
+
+ # Transports
+ config_validate_check_exists "config_transport_dir"
+ if [[ ! -d "${config_transport_dir}" ]]; then
+ config_dolog_fatal "The transport directory ${config_transport_dir} doesn't seem to exist"
+ envbot_quit 2
+ fi
+ config_validate_check_exists "config_transport"
+ if [[ ! -r "${config_transport_dir}/${config_transport}.sh" ]]; then
+ config_dolog_fatal "The transport ${config_transport} doesn't seem to exist"
+ envbot_quit 2
+ fi
+
+ # Modules
+ config_validate_check_exists config_modules_dir
+ if ! [[ -d "$config_modules_dir" ]]; then
+ if ! list_contains transport_supports "bind"; then
+ config_dolog_fatal "$config_modules_dir DOES NOT EXIST OR IS NOT A DIRECTORY."
+ envbot_quit 1
+ fi
+ fi
+ config_validate_check_exists config_modules
+}
+
+#---------------------------------------------------------------------
+## Validate some settings from config file that can only be done after
+## transport was loaded.
+## @Type Private
+#---------------------------------------------------------------------
+config_validate_transport() {
+ # At this point logging is enabled, we can use it.
+ if [[ $config_server_ssl -ne 0 ]]; then
+ if ! list_contains transport_supports "ssl"; then
+ log_fatal "THIS TRANSPORT DOES NOT SUPORT SSL"
+ envbot_quit 1
+ fi
+ else
+ if ! list_contains transport_supports "nossl"; then
+ log_fatal "THIS TRANSPORT REQUIRES SSL"
+ envbot_quit 1
+ fi
+ fi
+ if [[ "$config_server_bind" ]]; then
+ if ! list_contains transport_supports "bind"; then
+ log_fatal "THIS TRANSPORT DOES NOT SUPORT BINDING AN IP"
+ envbot_quit 1
+ fi
+ fi
+}
diff --git a/lib/debug.sh b/lib/debug.sh
new file mode 100644
index 0000000..106a329
--- /dev/null
+++ b/lib/debug.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Functions used during development for debugging.
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Debugging function to check that right number of parameters were
+## provided.
+## @param Lowest allowed count of parameters.
+## @param Higest allowed count of parameters. (Optional, defaults to same as lower)
+#---------------------------------------------------------------------
+debug_assert_argc() {
+ [[ $envbot_debugging ]] || return 0
+ if [[ ${BASH_ARGC[1]} -lt $1 || ${BASH_ARGC[1]} -gt ${2:-$1} ]]; then
+ log_debug "${FUNCNAME[1]} should have had $1 parameters but had ${BASH_ARGC[1]} instead"
+ log_debug "${FUNCNAME[1]} was called from ${BASH_SOURCE[2]}:${BASH_LINENO[1]} ${FUNCNAME[2]}."
+ return 1
+ fi
+}
+
+#---------------------------------------------------------------------
+## Reports who called function and with what arguments.
+## @Type API
+## @param Should be "$@" at first line of function.
+#---------------------------------------------------------------------
+debug_log_caller() {
+ [[ $envbot_debugging ]] || return 0
+ log_debug "${FUNCNAME[1]} called from ${BASH_SOURCE[2]}:${BASH_LINENO[1]} ${FUNCNAME[2]} with arguments: $*"
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Enable debugging.
+## @Type Private
+#---------------------------------------------------------------------
+debug_enable() {
+ envbot_debugging=1
+ shopt -s extdebug
+ log_debug "Debugging enabled"
+}
+
+#---------------------------------------------------------------------
+## Disable debugging.
+## @Type Private
+#---------------------------------------------------------------------
+debug_disable() {
+ envbot_debugging=''
+ shopt -u extdebug
+ log_debug "Debugging disabled"
+}
+
+#---------------------------------------------------------------------
+## Enable or disable debugging at startup.
+## @Type Private
+#---------------------------------------------------------------------
+debug_init() {
+ if [[ "$envbot_debugging" ]]; then
+ debug_enable
+ fi
+}
diff --git a/lib/feedback.sh b/lib/feedback.sh
new file mode 100644
index 0000000..75d2ec0
--- /dev/null
+++ b/lib/feedback.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## User feedback.
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Return a message that syntax was bad and what the correct syntax is.
+## @Type API
+## @param To who (nick or channel)
+## @param From what command
+## @param Syntax help
+#---------------------------------------------------------------------
+feedback_bad_syntax() {
+ send_notice "$1" "Syntax error. Correct syntax for $2 is $2 $3"
+}
+
+#---------------------------------------------------------------------
+## Return a message that something else was wrong in the command.
+## @Type API
+## @param To who (nick or channel)
+## @param From what function
+## @param Error message.
+#---------------------------------------------------------------------
+feedback_generic_error() {
+ send_notice "$1" "$2: Error: $3"
+}
+
+#---------------------------------------------------------------------
+## Return a message that a command was unknown.
+## @Type Private
+## @param Sender of message (n!u@h)
+## @param To where (botnick or channel)
+## @param Query
+#---------------------------------------------------------------------
+feedback_unknown_command() {
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ send_notice "$sendernick" "Error: Not able to parse this command: \"$3\". Are you sure you spelled it correctly?"
+}
diff --git a/lib/hash.sh b/lib/hash.sh
new file mode 100644
index 0000000..307ff90
--- /dev/null
+++ b/lib/hash.sh
@@ -0,0 +1,312 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Functions for working with associative arrays.
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Convert a string to hex
+## @Type Private
+## @param String to convert
+## @param Name of variable to return result in.
+#---------------------------------------------------------------------
+hash_hexify() {
+ # Res will contain full output string, hex current char.
+ local hex i res=
+ for ((i=0;i<${#1};i++)); do
+ # The ' is not documented in bash but it works.
+ # See http://www.opengroup.org/onlinepubs/009695399/utilities/printf.html
+ # for documentation of the ' syntax for printf.
+ printf -v hex '%x' "'${1:i:1}"
+ # Add to string
+ res+=$hex
+ done
+ # Print to variable.
+ printf -v "$2" '%s' "$res"
+}
+
+#---------------------------------------------------------------------
+## Convert a string from hex to normal
+## @Type Private
+## @param String to convert
+## @param Name of variable to return result in.
+#---------------------------------------------------------------------
+hash_unhexify() {
+ # Res will contain full output string, unhex current char.
+ local unhex i=0 res=
+ for ((i=0;i<${#1};i+=2)); do
+ # Convert back from hex. 2 chars at a time
+ # FIXME: This will break if output would be multibyte chars.
+ printf -v unhex \\"x${1:i:2}"
+ res+=$unhex
+ done
+ printf -v "$2" '%s' "$res"
+}
+
+#---------------------------------------------------------------------
+## Generate variable name for a item in the hash array.
+## @Type Private
+## @param Table name
+## @param Index
+## @param Name of variable to return result in.
+#---------------------------------------------------------------------
+hash_name_create() {
+ local hexindex
+ hash_hexify "$2" 'hexindex'
+ printf -v "$3" '%s' "hsh_${1}_${hexindex}"
+}
+
+#---------------------------------------------------------------------
+## Translate a variable name to an entry index name.
+## @param Variable name
+## @param Return value for index
+#---------------------------------------------------------------------
+hash_name_getindex() {
+ local unhexindex tablename indexname
+ local IFS="_"
+ read -r tablename indexname <<< "${1/hsh_//}"
+ unset IFS
+ hash_unhexify "$indexname" "$2"
+}
+
+
+#---------------------------------------------------------------------
+## Sets (overwrites any older) a value in a hash array
+## @Type API
+## @param Table name
+## @param Index
+## @param Value
+#---------------------------------------------------------------------
+hash_set() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ # Set it using the printf to variable
+ printf -v "$varname" '%s' "$3"
+}
+
+#---------------------------------------------------------------------
+## Append a value to the end of an entry in a hash array
+## @Type API
+## @param Table name
+## @param Index
+## @param Value to append
+## @param Separator (optional, defaults to space)
+#---------------------------------------------------------------------
+hash_append() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ # Append to end, or if empty just set.
+ if [[ "${!varname}" ]]; then
+ local sep=${4:-" "}
+ printf -v "$varname" '%s' "${!varname}${sep}${3}"
+ else
+ printf -v "$varname" '%s' "$3"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Opposite of <@function hash_append>, removes a value from a list
+## in a hash entry
+## @Type API
+## @param Table name
+## @param Index
+## @param Value to remove
+## @param Separator (optional, defaults to space)
+#---------------------------------------------------------------------
+hash_substract() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ # If not empty try to remove value
+ if [[ "${!varname}" ]]; then
+ local sep=${4:-" "}
+ # FIXME: substrings of the entries in the list may match :/
+ local list="${!varname}"
+ list="${list//$3}"
+ # Remove any double $sep caused by this.
+ list="${list//$sep$sep/$sep}"
+ printf -v "$varname" '%s' "$list"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Replace a value in list style hash entry.
+## @Type API
+## @param Table name
+## @param Index
+## @param Value to replace
+## @param Value to replace with
+## @param Separator (optional, defaults to space)
+#---------------------------------------------------------------------
+hash_replace() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ # Append to end, or if empty just set.
+ local sep=${5:-" "}
+ if [[ "${!varname}" =~ (^|$sep)${3}($sep|$) ]]; then
+ # FIXME: substrings of the entries in the list may match :/
+ local list="${!varname}"
+ list="${list//$3/$4}"
+ printf -v "$varname" '%s' "$list"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Removes an entry (if it exists) from a hash array
+## @Note If the entry does not exist, nothing will happen
+## @Type API
+## @param Table name
+## @param Index
+#---------------------------------------------------------------------
+hash_unset() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ unset "${varname}"
+}
+
+#---------------------------------------------------------------------
+## Gets a value (if it exists) from a hash array
+## @Note If value does not exist, the variable will be empty.
+## @Type API
+## @param Table name
+## @param Index
+## @param Name of variable to return result in.
+#---------------------------------------------------------------------
+hash_get() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ # Now print out to variable using indirect ref to get the value.
+ printf -v "$3" '%s' "${!varname}"
+}
+
+#---------------------------------------------------------------------
+## Check if a list style hash entry contains a specific value.
+## @Type API
+## @param Table name
+## @param Index
+## @param Value to check for
+## @param Separator (optional, defaults to space)
+## @return 0 Found
+## @return 1 Not found (or hash doesn't exist).
+#---------------------------------------------------------------------
+hash_contains() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+
+ local sep=${4:-" "}
+ if [[ "${sep}${!varname}${sep}" =~ ${sep}${3}${sep} ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+#---------------------------------------------------------------------
+## Check if a any space separated entry in a hash array contains
+## a specific value.
+## @Type API
+## @param Table name
+## @param Value to check for
+## @return 0 Found
+## @return 1 Not found (or hash doesn't exist).
+#---------------------------------------------------------------------
+hash_search() {
+ # Get variable names
+ eval "local vars=\"\${!hsh_${1}_*}\""
+ # Append to end, or if empty just set.
+ if [[ $vars ]]; then
+ local var
+ # Extract index.
+ for var in $vars; do
+ [[ "${!varname}" =~ (^| )${2}( |$) ]] && return 0
+ done
+ fi
+ return 1
+}
+
+#---------------------------------------------------------------------
+## Check if an entry exists in a hash array
+## @Type API
+## @param Table name
+## @param Index
+## @return 0 If the entry exists
+## @return 1 If the entry doesn't exist
+#---------------------------------------------------------------------
+hash_exists() {
+ local varname
+ hash_name_create "$1" "$2" 'varname'
+ # This will return the return code we want.
+ [[ "${!varname}" ]]
+}
+
+#---------------------------------------------------------------------
+## Removes an entire hash array
+## @Type API
+## @param Table name
+## @return 0 Ok
+## @return 1 Other error
+## @return 2 Table not found
+#---------------------------------------------------------------------
+hash_reset() {
+ # Get all variables with a prefix
+ eval "local vars=\"\${!hsh_${1}_*}\""
+ # If any variable, unset them.
+ if [[ $vars ]]; then
+ unset ${vars} || return 1
+ else
+ return 2
+ fi
+}
+
+#---------------------------------------------------------------------
+## Returns a space separated list of the indices of a hash array
+## @Type API
+## @param Table name
+## @param Name of variable to return result in.
+## @return 0 Ok
+## @return 1 Other error
+## @return 2 Table not found
+#---------------------------------------------------------------------
+hash_get_indices() {
+ # Get all variables with a prefix
+ eval "local vars=\"\${!hsh_${1}_*}\""
+ # If any variable loop through and get the "normal" index.
+ if [[ $vars ]]; then
+ local var unhexname returnlist
+ # Extract index.
+ for var in $vars; do
+ hash_name_getindex "$var" 'unhexname'
+ returnlist+=" $unhexname"
+ done
+ # Return them in variable.
+ printf -v "$2" '%s' "${returnlist}"
+ return 0
+ else
+ return 2
+ fi
+}
diff --git a/lib/log.sh b/lib/log.sh
new file mode 100644
index 0000000..cfef6fd
--- /dev/null
+++ b/lib/log.sh
@@ -0,0 +1,285 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Logging API
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Log a fatal error to the main log file as well as STDOUT.
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_fatal() {
+ log "FATAL " "$log_color_fatal" "$1" 1
+}
+
+#---------------------------------------------------------------------
+## Log a fatal error to a specific log file as well as
+## the main log file and STDOUT.
+## @Type API
+## @param The extra log file (relative to the current log dir)
+## @param The log message to log
+#---------------------------------------------------------------------
+log_fatal_file() {
+ log "FATAL " "$log_color_fatal" "$2" 1 "$1"
+}
+
+
+#---------------------------------------------------------------------
+## Log an error to the main log file as well as STDOUT.
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_error() {
+ log "ERROR " "$log_color_error" "$1" 1
+}
+
+#---------------------------------------------------------------------
+## Log an error to a specific log file as well as
+## the main log file and STDOUT.
+## @Type API
+## @param The extra log file (relative to the current log dir)
+## @param The log message to log
+#---------------------------------------------------------------------
+log_error_file() {
+ log "ERROR " "$log_color_error" "$2" 1 "$1"
+}
+
+
+#---------------------------------------------------------------------
+## Log a warning to the main log file as well as STDOUT.
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_warning() {
+ log "WARNING " "$log_color_warning" "$1" 1
+}
+
+#---------------------------------------------------------------------
+## Log a warning to a specific log file as well as
+## the main log file and STDOUT.
+## @Type API
+## @param The extra log file (relative to the current log dir)
+## @param The log message to log
+#---------------------------------------------------------------------
+log_warning_file() {
+ log "WARNING " "$log_color_warning" "$2" 1 "$1"
+}
+
+
+#---------------------------------------------------------------------
+## Log an info message to the main log file.
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_info() {
+ log "INFO " "$log_color_info" "$1" 0
+}
+
+#---------------------------------------------------------------------
+## Log an info message to the main log file and STDOUT.
+## Normally this shouldn't be used by modules.
+## It is used for things like "Connecting"
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_info_stdout() {
+ log "INFO " "$log_color_info" "$1" 1
+}
+
+#---------------------------------------------------------------------
+## Log an info message to a specific log file as well as
+## the main log file and STDOUT.
+## Normally this shouldn't be used by modules.
+## It is used for things like "Connecting"
+## @Type API
+## @param The extra log file (relative to the current log dir)
+## @param The log message to log
+#---------------------------------------------------------------------
+log_info_stdout_file() {
+ log "INFO " "$log_color_info" "$2" 1 "$1"
+}
+
+#---------------------------------------------------------------------
+## Log an info message to a specific log file as well as STDOUT.
+## @Type API
+## @param The extra log file (relative to the current log dir)
+## @param The log message to log
+#---------------------------------------------------------------------
+log_info_file() {
+ log "INFO " "$log_color_info" "$2" 0 "$1"
+}
+
+#---------------------------------------------------------------------
+## Log a debug message.
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_debug() {
+ log "DEBUG " "" "$1" 0 debug.log
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Logging prefix
+## @Type Private
+#---------------------------------------------------------------------
+log_prefix="-"
+
+#---------------------------------------------------------------------
+## Get human readable date.
+## @Type Private
+## @Stdout Human readable date
+#---------------------------------------------------------------------
+log_get_date() {
+ date +'%Y-%m-%d %k:%M:%S'
+}
+
+#---------------------------------------------------------------------
+## Get escape codes from tput
+## @Type Private
+## @param capname
+## @param Return variable name
+## @return 0 OK
+## @return 1 Not supported or unknown cap
+## @Note Return variable will be unset if the value is not supported
+#---------------------------------------------------------------------
+log_check_cap() {
+ tput $1 >/dev/null 2>&1
+ if [[ $? -eq 0 ]]; then
+ printf -v "$2" '%s' "$(tput $1)"
+ else
+ printf -v "$2" '%s' ''
+ fi
+}
+
+
+#---------------------------------------------------------------------
+## Log, internal to this file.
+## @Type Private
+## @param Level to log at (ERROR or such, aligned to space)
+## @param Color of level
+## @param The log message to log
+## @param Force log to stdout (0 or 1)
+## @param Optional extra file to log to.
+#---------------------------------------------------------------------
+log() {
+ # Log file is set?
+ [[ $log_file ]] || return 0
+ # Log date.
+ local logdate="$(log_get_date)"
+ # ncm = No Color Message
+ local ncm="$log_prefix $logdate ${1}${3}"
+ echo "$ncm" >> "$log_file"
+ # Extra log file?
+ [[ $5 ]] && echo "$ncm" >> "$log_dir/$5"
+ # STDOUT?
+ if [[ $config_log_stdout -eq 1 || $4 -eq 1 ]]; then
+ # Colors and then get rid of bell chars.
+ echo "${log_color_std}${log_prefix}${log_color_none} $logdate ${2}${1}${log_color_none}${3//$'\007'}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Used internally in core to log raw line
+## @Type Private
+## @param Line to log
+#---------------------------------------------------------------------
+log_raw_in() {
+ [[ $config_log_raw = 1 ]] && log_raw "<" "$log_color_in" "$1"
+}
+#---------------------------------------------------------------------
+## Used internally in core to log raw line
+## @Type Private
+## @param Line to log
+#---------------------------------------------------------------------
+log_raw_out() {
+ [[ $config_log_raw = 1 ]] && log_raw ">" "$log_color_out" "$1"
+}
+
+
+#---------------------------------------------------------------------
+## Internal function to this file.
+## @Type Private
+## @param Prefix to use
+## @param Color of prefix
+## @param Message to log
+#---------------------------------------------------------------------
+log_raw() {
+ # Log file is set?
+ [[ $log_file ]] || return 0
+ # No Color Message
+ # Log date.
+ local logdate="$(log_get_date)"
+ # No colors for file
+ echo "$1 $logdate $3" >> "$log_dir/raw.log"
+ # STDOUT?
+ if [[ $config_log_stdout -eq 1 ]]; then
+ # Get rid of bell chars.
+ echo "${2}${1}${log_color_none} $logdate RAW ${3//$'\007'}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Create log file.
+## @Type Private
+#---------------------------------------------------------------------
+log_init() {
+ local now
+ time_get_current 'now'
+ # This creates log dir for this run:
+ log_dir="${config_log_dir}/${now}"
+ # Security, the log may contain passwords.
+ mkdir -m 700 "$log_dir"
+ if [[ $? -ne 0 ]]; then
+ echo "Error: couldn't create log dir"
+ envbot_quit 1
+ fi
+ log_file="${log_dir}/main.log"
+ touch "$log_file"
+ if [[ $? -ne 0 ]]; then
+ echo "Error: couldn't create logfile"
+ envbot_quit 1
+ fi
+
+ # Should there be colors?
+ if [[ $config_log_colors -eq 1 ]]; then
+ local bold
+ # Generate colors
+ log_check_cap sgr0 log_color_none # No colour
+ log_check_cap bold bold # Bold local
+ log_check_cap 'setaf 1' log_color_error # Red
+ log_color_fatal="${log_color_error}${bold}" # Red bold
+ log_check_cap 'setaf 3' log_color_warning # Yellow
+ log_check_cap 'setaf 2' log_color_info # Green
+ log_check_cap 'setaf 4' log_color_std # Blue bold, for standard prefix
+ log_color_std+="${bold}"
+ log_check_cap 'setaf 5' log_color_in # Magenta, for prefix
+ log_check_cap 'setaf 6' log_color_out # Cyan, for prefix
+ fi
+
+ log_info_stdout "Log directory is $log_dir"
+}
diff --git a/lib/main.sh b/lib/main.sh
new file mode 100644
index 0000000..ddc9103
--- /dev/null
+++ b/lib/main.sh
@@ -0,0 +1,566 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## This is the main file, it should be called with a wrapper (envbot)
+#---------------------------------------------------------------------
+
+
+###################
+# #
+# Sanity checks #
+# #
+###################
+
+# Error to fail with for old bash.
+fail_old_bash() {
+ echo "Sorry your bash version is too old!"
+ echo "You need at least version 3.2.10 of bash"
+ echo "Please install a newer version:"
+ echo " * Either use your distro's packages"
+ echo " * Or see http://www.gnu.org/software/bash/"
+ exit 2
+}
+
+# Check bash version. We need at least 3.2.10
+# Lets not use anything like =~ here because
+# that may not work on old bash versions.
+if [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -lt 32 ]]; then
+ fail_old_bash
+elif [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -eq 32 && "${BASH_VERSINFO[2]}" -lt 10 ]]; then
+ fail_old_bash
+fi
+
+# We should not run as root.
+if [[ $EUID -eq 0 ]]; then
+ echo "ERROR: Don't run envbot as root. Please run it under a normal user. Really."
+ exit 1
+fi
+
+######################
+# #
+# Set up variables #
+# #
+######################
+
+# Version and URL
+#---------------------------------------------------------------------
+## Version of envbot.
+## @Type API
+## @Read_only Yes
+#---------------------------------------------------------------------
+declare -r envbot_version='0.1-beta1'
+#---------------------------------------------------------------------
+## Homepage of envbot.
+## @Type API
+## @Read_only Yes
+#---------------------------------------------------------------------
+declare -r envbot_homepage='http://envbot.org'
+
+##############
+# #
+# Sane env #
+# #
+##############
+
+# Set some variables to make bot work sane
+# For example tr + some LC_COLLATE = breaks in some cases.
+unset LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY
+unset LC_MESSAGES LC_PAPER LC_NAME LC_ADDRESS
+unset LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION
+export LC_ALL=C
+export LANG=C
+
+# Some of these may be overkill, but better be on
+# safe side.
+set +amu
+set -f
+shopt -u sourcepath hostcomplete progcomp xpg_echo dotglob
+shopt -u nocasematch nocaseglob nullglob
+shopt -s extquote promptvars extglob
+
+# If you need some other PATH, override in top of config...
+export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+
+# To make set -x more usable
+export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]} : '
+
+
+# This is needed when we run the bot with env -i as recommended.
+declare -r tmp_home="$(mktemp -dt envbot.home.XXXXXXXXXX)"
+# I don't want to end up with rm -rf $HOME in case it is something
+# else at that point, so lets use another variable.
+
+# Temp trap on ctrl-c until the next "stage" of trap gets loaded (at connect)
+trap 'rm -rvf "$tmp_home"; exit 1' TERM INT
+
+#---------------------------------------------------------------------
+## Now create a temp function to quit on problems in a way that cleans up
+## temp stuff until we have loaded enough to use the normal function bot_quit.
+## @param Return status of bot
+#---------------------------------------------------------------------
+envbot_quit() {
+ rm -rf "$tmp_home"
+ exit "$1"
+}
+
+# And finally lets export this as $HOME
+export HOME="$tmp_home"
+
+#---------------------------------------------------------------------
+## Will be set to 1 if -v or --verbose is passed
+## on command line.
+## @Type Private
+#---------------------------------------------------------------------
+force_verbose=0
+
+#---------------------------------------------------------------------
+## Store command line for later use
+## @Type Private
+#---------------------------------------------------------------------
+command_line=( "$@" )
+
+# Some constants used in different places
+
+#---------------------------------------------------------------------
+## Current config version.
+## @Type API
+## @Read_only Yes
+#---------------------------------------------------------------------
+declare -r config_current_version=17
+
+#---------------------------------------------------------------------
+## In progress of quitting? This is used to
+## work around the issue in bug 25.<br />
+## -1 means not even in main loop yet.
+## @Type Private
+#---------------------------------------------------------------------
+envbot_quitting=-1
+
+#---------------------------------------------------------------------
+## If empty debugging is turned off. If not empty it is on.
+#---------------------------------------------------------------------
+envbot_debugging=''
+
+#---------------------------------------------------------------------
+## Print help message
+## @Type Private
+#---------------------------------------------------------------------
+print_cmd_help() {
+ echo 'envbot is an advanced modular IRC bot coded in bash.'
+ echo ''
+ echo 'Usage: envbot [OPTION]...'
+ echo ''
+ echo 'Options:'
+ echo ' -c, --config file Use file instead of the default as config file.'
+ echo ' -l, --libdir directory Use directory instead of the default as library directory.'
+ echo ' -v, --verbose Force verbose output even if config_log_stdout is 0.'
+ echo ' -d, --debug Enable debugging code. Most likely pointless to anyone'
+ echo ' except envbot developers or module developers.'
+ echo ' -h, --help Display this help and exit'
+ echo ' -V, --version Output version information and exit'
+ echo ''
+ echo "Note that envbot can't handle short versions of options being written together like"
+ echo "-vv currently."
+ echo ''
+ echo 'Exit status is 0 if OK, 1 if minor problems, 2 if serious trouble.'
+ echo ''
+ echo 'Examples:'
+ echo ' envbot Runs envbot with default options.'
+ echo ' envbot -c bot.config Runs envbot with the config bot.config.'
+ echo ''
+ echo "Report bugs to ${envbot_homepage}/trac/simpleticket"
+ envbot_quit 0
+}
+
+#---------------------------------------------------------------------
+## Print version message
+## @Type Private
+#---------------------------------------------------------------------
+print_version() {
+ echo "envbot $envbot_version - An advanced modular IRC bot in bash."
+ echo ''
+ echo 'Copyright (C) 2007-2008 Arvid Norlander'
+ echo 'Copyright (C) 2007-2008 EmErgE'
+ echo 'This is free software; see the source for copying conditions. There is NO'
+ echo 'warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.'
+ echo ''
+ echo 'Written by Arvid Norlander and EmErgE.'
+ envbot_quit 0
+}
+
+# Parse any command line arguments.
+if [[ $# -gt 0 ]]; then
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ '--help'|'-help'|'--usage'|'-usage'|'-h')
+ print_cmd_help
+ ;;
+ '--config'|'-c')
+ config_file="$2"
+ shift 2
+ ;;
+ '--debug'|'-d')
+ envbot_debugging=1
+ shift 1
+ ;;
+ '--libdir'|'-l')
+ library_dir="$2"
+ shift 2
+ ;;
+ '--verbose'|'-v')
+ force_verbose=1
+ shift 1
+ ;;
+ '--version'|'-V')
+ print_version
+ ;;
+ *)
+ print_cmd_help
+ ;;
+ esac
+ done
+fi
+
+echo "Loading... Please wait"
+
+if [[ -z "$config_file" ]]; then
+ echo "ERROR: No config file set, you probably didn't use the wrapper program to start envbot"
+ envbot_quit 1
+fi
+
+if [[ ! -r "$config_file" ]]; then
+ echo "ERROR: Can't read config file ${config_file}."
+ echo "Check that it is really there and correct permissions are set."
+ echo "If you used --config to specify name of config file, check that you spelled it correctly."
+ envbot_quit 1
+fi
+
+echo "Loading config"
+source "$config_file"
+if [[ $? -ne 0 ]]; then
+ echo "Error: couldn't load config from $config_file"
+ envbot_quit 1
+fi
+
+# This is hackish, it should be in config.sh (config_validate)
+# The reason is that we need to check some things before we can load config.sh
+if [[ -z "$config_version" ]]; then
+ echo "ERROR: YOU MUST SET THE CORRECT config_version IN THE CONFIG"
+ envbot_quit 2
+fi
+if [[ $config_version -ne $config_current_version ]]; then
+ echo "ERROR: YOUR config_version IS $config_version BUT THE BOT'S CONFIG VERSION IS $config_current_version."
+ echo "PLEASE UPDATE YOUR CONFIG. Check bot_settings.sh.example for current format."
+ envbot_quit 2
+fi
+
+# Force verbose output if -v or --verbose was on
+# command line.
+if [[ $force_verbose -eq 1 ]]; then
+ config_log_stdout='1'
+fi
+
+# Must be checked here and not in validate_config because of
+# loading order.
+if [[ -z "$library_dir" ]]; then
+ echo "ERROR: No library directory set, you probably didn't use the wrapper program to start envbot"
+ envbot_quit 1
+fi
+
+if [[ ! -d "$library_dir" ]]; then
+ echo "ERROR: library directory $library_dir does not exist, is not a directory or can't be read for some other reason."
+ echo "Check that it is really there and correct permissions are set."
+ echo "If you used --libdir to specify location of library directory, check that you spelled it correctly."
+ envbot_quit 2
+fi
+
+echo "Loading library functions"
+# Load library functions.
+libraries="hash time log send feedback numerics channels parse \
+ access misc config commands modules server debug"
+for library in $libraries; do
+ source "${library_dir}/${library}.sh"
+done
+unset library
+
+# Validate other config variables.
+config_validate
+time_init
+log_init
+debug_init
+
+log_info_stdout "Loading transport"
+source "${config_transport_dir}/${config_transport}.sh"
+if [[ $? -ne 0 ]]; then
+ log_fatal "Couldn't load transport. Couldn't load the file..."
+ envbot_quit 2
+fi
+
+if ! transport_check_support; then
+ log_fatal "The transport reported it can't work on this system or with this configuration."
+ log_fatal "Please read any other errors displayed above and consult documentation for the transport module you are using."
+ envbot_quit 2
+fi
+
+# Now logging functions can be used.
+
+# Load modules
+
+log_info_stdout "Loading modules"
+# Load modules
+modules_load_from_config
+
+#---------------------------------------------------------------------
+## This can be used when the code does not need exact time.
+## It will be updated each time the bot get a new line of
+## data.
+## @Type API
+#---------------------------------------------------------------------
+envbot_time=''
+server_connected_before=0
+while true; do
+ # In progress of quitting? This is used to
+ # work around the issue in bug 25.
+ envbot_quitting=0
+ for module in $modules_before_connect; do
+ module_${module}_before_connect
+ done
+
+ if [[ $server_connected_before -ne 0 ]]; then
+ # We got here by being connected before and
+ # loosing connection, keep retrying
+ while true; do
+ if server_connect; then
+ server_connected_before=1
+ break
+ else
+ log_error "Failed to reconnect, trying again in 20 seconds"
+ sleep 20
+ fi
+ done
+ else
+ # In this case abort on failure to connect, likely bad config.
+ # and most likely the user is present to fix it.
+ # If someone disagrees I may change it.
+ server_connect || {
+ log_error "Connection failed"
+ envbot_quit 1
+ }
+ server_connected_before=1
+ fi
+ trap 'bot_quit "Interrupted (Ctrl-C)"' INT
+ trap 'bot_quit "Terminated (SIGTERM)"' TERM
+ for module in $modules_after_connect; do
+ module_${module}_after_connect
+ done
+
+ while true; do
+ line=
+ transport_read_line
+ transport_status="$?"
+ # Still connected?
+ if ! transport_alive; then
+ break
+ fi
+ time_get_current 'envbot_time'
+
+ # Did we timeout waiting for data
+ # or did we get data?
+ if [[ $transport_status -ne 0 ]]; then
+ continue
+ fi
+
+ log_raw_in "$line"
+ for module in $modules_on_raw; do
+ module_${module}_on_raw "$line"
+ if [[ $? -ne 0 ]]; then
+ # TODO: Check that this does what it should.
+ continue 2
+ fi
+ done
+ if [[ $line =~ ^:${server_name}\ +([0-9]{3})\ +([^ ]+)\ +(.*) ]]; then
+ # this is a numeric
+ numeric="${BASH_REMATCH[1]}"
+ numericdata="${BASH_REMATCH[3]}"
+ server_handle_numerics "$numeric" "${BASH_REMATCH[2]}" "$numericdata"
+ for module in $modules_on_numeric; do
+ module_${module}_on_numeric "$numeric" "$numericdata"
+ if [[ $? -ne 0 ]]; then
+ break
+ fi
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +PRIVMSG\ +([^:]+)\ +:(.*) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ target="${BASH_REMATCH[2]}"
+ query="${BASH_REMATCH[3]}"
+ # Check if there is a command.
+ commands_call_command "$sender" "$target" "$query"
+ # Check return code
+ case $? in
+ 1)
+ continue
+ ;;
+ 2)
+ if [[ $config_feedback_unknown_commands -eq 0 ]]; then
+ continue
+ elif [[ $config_feedback_unknown_commands -eq 1 ]]; then
+ feedback_unknown_command "$sender" "$target" "$query"
+ fi
+ ;;
+ esac
+ for module in $modules_on_PRIVMSG; do
+ module_${module}_on_PRIVMSG "$sender" "$target" "$query"
+ if [[ $? -ne 0 ]]; then
+ break
+ fi
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +NOTICE\ +([^:]+)\ +:(.*) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ target="${BASH_REMATCH[2]}"
+ query="${BASH_REMATCH[3]}"
+ for module in $modules_on_NOTICE; do
+ module_${module}_on_PRIVMSG "$sender" "$target" "$query"
+ if [[ $? -ne 0 ]]; then
+ break
+ fi
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +TOPIC\ +(#[^ ]+)(\ +:(.*))? ]]; then
+ sender="${BASH_REMATCH[1]}"
+ channel="${BASH_REMATCH[2]}"
+ topic="${BASH_REMATCH[4]}"
+ for module in $modules_on_TOPIC; do
+ module_${module}_on_TOPIC "$sender" "$channel" "$topic"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +MODE\ +(#[^ ]+)\ +(.+) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ channel="${BASH_REMATCH[2]}"
+ modes="${BASH_REMATCH[3]}"
+ for module in $modules_on_channel_MODE ; do
+ module_${module}_on_channel_MODE "$sender" "$channel" "$modes"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +MODE\ +([^# ]+)\ +(.+) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ target="${BASH_REMATCH[2]}"
+ modes="${BASH_REMATCH[3]}"
+ for module in $modules_on_user_MODE ; do
+ module_${module}_on_user_MODE "$sender" "$target" "$modes"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +INVITE\ +([^ ]+)\ +:?(.+) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ target="${BASH_REMATCH[2]}"
+ channel="${BASH_REMATCH[3]}"
+ for module in $modules_on_INVITE; do
+ module_${module}_on_INVITE "$sender" "$target" "$channel"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +NICK\ +:?(.+) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ newnick="${BASH_REMATCH[2]}"
+ # Check if it was our own nick
+ server_handle_nick "$sender" "$newnick"
+ for module in $modules_on_NICK; do
+ module_${module}_on_NICK "$sender" "$newnick"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +JOIN\ +:?(.*) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ channel="${BASH_REMATCH[2]}"
+ # Check if it was our own nick that joined
+ channels_handle_join "$sender" "$channel"
+ for module in $modules_on_JOIN; do
+ module_${module}_on_JOIN "$sender" "$channel"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +PART\ +(#[^ ]+)(\ +:(.*))? ]]; then
+ sender="${BASH_REMATCH[1]}"
+ channel="${BASH_REMATCH[2]}"
+ reason="${BASH_REMATCH[4]}"
+ # Check if it was our own nick that parted
+ channels_handle_part "$sender" "$channel" "$reason"
+ for module in $modules_on_PART; do
+ module_${module}_on_PART "$sender" "$channel" "$reason"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +KICK\ +(#[^ ]+)\ +([^ ]+)(\ +:(.*))? ]]; then
+ sender="${BASH_REMATCH[1]}"
+ channel="${BASH_REMATCH[2]}"
+ kicked="${BASH_REMATCH[3]}"
+ reason="${BASH_REMATCH[5]}"
+ # Check if it was our own nick that got kicked
+ channels_handle_kick "$sender" "$channel" "$kicked" "$reason"
+ for module in $modules_on_KICK; do
+ module_${module}_on_KICK "$sender" "$channel" "$kicked" "$reason"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +QUIT(\ +:(.*))? ]]; then
+ sender="${BASH_REMATCH[1]}"
+ reason="${BASH_REMATCH[3]}"
+ for module in $modules_on_QUIT; do
+ module_${module}_on_QUIT "$sender" "$reason"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +KILL\ +([^ ]*)\ +:([^ ]*)\ +\((.*)\) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ target="${BASH_REMATCH[2]}"
+ path="${BASH_REMATCH[3]}"
+ reason="${BASH_REMATCH[4]}"
+ # I don't think we need to check if we were the target or not,
+ # the bot doesn't need to care as far as I can see.
+ for module in $modules_on_KILL; do
+ module_${module}_on_KILL "$sender" "$target" "$path" "$reason"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +PONG\ +([^ ]*)\ +:?(.*)$ ]]; then
+ sender="${BASH_REMATCH[1]}"
+ server2="${BASH_REMATCH[2]}"
+ data="${BASH_REMATCH[3]}"
+ for module in $modules_on_PONG; do
+ module_${module}_on_PONG "$sender" "$server2" "$data"
+ done
+ elif [[ $line =~ ^[^:] ]] ;then
+ # ERROR?
+ if [[ "$line" =~ ^ERROR\ +:(.*) ]]; then
+ error="${BASH_REMATCH[1]}"
+ log_error "Got ERROR from server: $error"
+ for module in $modules_on_server_ERROR; do
+ module_${module}_on_server_ERROR "$error"
+ done
+ # If we get an ERROR we can assume we are disconnected.
+ break
+ # PING? If not report as unhandled
+ elif ! server_handle_ping "$line"; then
+ log_info_file unknown_data.log "A non-sender prefixed line that didn't match any hook: $line"
+ fi
+ else
+ log_info_file unknown_data.log "Something that didn't match any hook: $line"
+ fi
+ done
+ if [[ $envbot_quitting -ne 0 ]]; then
+ # Hm, a trap got aborted it seems.
+ # Trying to handle this.
+ log_info "Quit trap got aborted: envbot_quitting=${envbot_quitting}. Recovering"
+ bot_quit
+ break
+ fi
+ log_error 'DIED FOR SOME REASON'
+ transport_disconnect
+ server_connected=0
+ for module in $modules_after_disconnect; do
+ module_${module}_after_disconnect
+ done
+ # Don't reconnect right away. We might get throttled and other nasty stuff.
+ sleep 10
+done
+rm -rf "$tmp_home"
diff --git a/lib/misc.sh b/lib/misc.sh
new file mode 100644
index 0000000..861ae6a
--- /dev/null
+++ b/lib/misc.sh
@@ -0,0 +1,267 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Misc functions.
+#---------------------------------------------------------------------
+
+
+# Some codes for IRC formatting
+#---------------------------------------------------------------------
+## IRC formatting: Bold
+## @Type API
+#---------------------------------------------------------------------
+format_bold=$'\002'
+#---------------------------------------------------------------------
+## IRC formatting: Underline
+## @Type API
+#---------------------------------------------------------------------
+format_underline=$'\037'
+#---------------------------------------------------------------------
+## IRC formatting: Color
+## @Type API
+#---------------------------------------------------------------------
+format_color=$'\003'
+#---------------------------------------------------------------------
+## IRC formatting: Inverse
+## @Type API
+#---------------------------------------------------------------------
+format_inverse=$'\026'
+#---------------------------------------------------------------------
+## IRC formatting: Restore to normal
+## @Type API
+#---------------------------------------------------------------------
+format_normal=$'\017'
+#---------------------------------------------------------------------
+## IRC formatting: ASCII bell
+## Please. Don't. Abuse. This.
+## @Type API
+#---------------------------------------------------------------------
+format_bell=$'\007'
+
+# Color table:
+# white 0
+# black 1
+# blue 2
+# green 3
+# red 4
+# darkred 5
+# purple 6
+# darkyellow 7
+# yellow 8
+# brightgreen 9
+# darkaqua 10
+# aqua 11
+# lightblue 12
+# brightpurple 13
+# darkgrey 14
+# lightgrey 15
+
+#---------------------------------------------------------------------
+## This will add colors around this text.
+## @Type API
+## @param Foreground color
+## @param Background color
+## @param String to colorise
+#---------------------------------------------------------------------
+format_colorise() {
+ echo "${format_color}${1},${2}${3}${format_normal}"
+}
+
+#---------------------------------------------------------------------
+## Quits the bot in a graceful way.
+## @Type API
+## @param Reason to quit (optional)
+## @param Return status (optional, if not given, then exit 0).
+#---------------------------------------------------------------------
+bot_quit() {
+ # Yes this function is odd but there is a reason.
+ # If this is called from a trap like Ctrl-C we must be able to
+ # resume.
+ # Keep track of in what state we are
+ while true; do
+ case "$envbot_quitting" in
+ 0)
+ for module in $modules_before_disconnect; do
+ module_${module}_before_disconnect
+ done
+ (( envbot_quitting++ ))
+ ;;
+ 1)
+ local reason="$1"
+ send_quit "$reason"
+ sleep 1
+ (( envbot_quitting++ ))
+ ;;
+ 2)
+ server_connected=0
+ for module in $modules_after_disconnect; do
+ module_${module}_after_disconnect
+ done
+ (( envbot_quitting++ ))
+ ;;
+ 3)
+ for module in $modules_FINALISE; do
+ module_${module}_FINALISE
+ done
+ (( envbot_quitting++ ))
+ ;;
+ 4)
+ log_info_stdout "Bot quit gracefully"
+ transport_disconnect
+ (( envbot_quitting++ ))
+ ;;
+ # -1 is before main loop entered,
+ # may happen during module loading
+ 5|-1)
+ rm -rvf "$tmp_home"
+ if [[ $2 ]]; then
+ exit $2
+ else
+ exit 0
+ fi
+ ;;
+ *)
+ log_error "Um. bot_quit() and envbot_quitting is $envbot_quitting. This shouldn't happen."
+ log_error "Please report a bug including the last 40 lines or so of log and what you did to cause it."
+ # Quit and clean up temp files.
+ envbot_quit 2
+ ;;
+ esac
+ done
+}
+
+#---------------------------------------------------------------------
+## Restart the bot in a graceful way. I hope.
+## @Type API
+## @param Reason to restart (optional)
+#---------------------------------------------------------------------
+bot_restart() {
+ for module in $modules_before_disconnect; do
+ module_${module}_before_disconnect
+ done
+ local reason="$1"
+ send_quit "$reason"
+ sleep 1
+ server_connected=0
+ for module in $modules_after_disconnect; do
+ module_${module}_after_disconnect
+ done
+ for module in $modules_FINALISE; do
+ module_${module}_FINALISE
+ done
+ log_info_stdout "Bot quit gracefully"
+ transport_disconnect
+ rm -rvf "$tmp_home"
+ exec env -i TERM="$TERM" "$(type -p bash)" $0 "${command_line[@]}"
+}
+
+
+#---------------------------------------------------------------------
+## Strip leading/trailing spaces.
+## @Type API
+## @Note Before this function was deprecated, but it has been recoded
+## @Note in a much faster way. This version is not compatible with old
+## @Note version.
+## @param String to strip
+## @param Variable to return in
+#---------------------------------------------------------------------
+misc_clean_spaces() {
+ # Fastest way that is still secure
+ local array
+ read -ra array <<< "$1"
+ printf -v "$2" '%s' "${array[*]}"
+}
+
+#---------------------------------------------------------------------
+## Strip leading/trailing separator.
+## @Type API
+## @param String to strip
+## @param Variable to return in
+## @param Separator
+#---------------------------------------------------------------------
+misc_clean_delimiter() {
+ local sep="$3" array
+ local IFS="$sep"
+ # Fastest way that is still secure
+ read -ra array <<< "$1"
+ local tmp="${array[*]}"
+ printf -v "$2" '%s' "${tmp#${sep}}"
+}
+
+#---------------------------------------------------------------------
+## Remove a value from a space (or other delimiter) separated list.
+## @Type API
+## @param List to remove from.
+## @param Value to remove.
+## @param Variable to return new list in.
+## @param Separator (optional, defaults to space)
+#---------------------------------------------------------------------
+list_remove() {
+ local sep=${4:-" "}
+ local oldlist="${sep}${!1}${sep}"
+ local newlist="${oldlist//${sep}${2}${sep}/${sep}}"
+ misc_clean_delimiter "$newlist" "$3" "$sep" # Get rid of the unneeded spaces.
+}
+
+#---------------------------------------------------------------------
+## Checks if a space separated list contains a value.
+## @Type API
+## @param List to check.
+## @param Value to check for.
+## @return 0 If found.
+## @return 1 If not found.
+#---------------------------------------------------------------------
+list_contains() {
+ [[ " ${!1} " = *" $2 "* ]]
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Like debug_assert_argc but works without debugging on.
+## For use in sensitive functions in core.
+## @Type Private
+## @param Minimum count of parameters
+## @param Maximum count of parameters
+## @param All the rest of the parameters as "$@"
+## @Example For example this could be called as:
+## @Example <pre>
+## @Example foo() {
+## @Example security_assert_argc 2 2 "$@"
+## @Example ... rest of function ...
+## @Example }
+## @Example </pre>
+#---------------------------------------------------------------------
+security_assert_argc() {
+ local min="$1" max="$2"
+ shift 2
+ if [[ $# -lt $min || $# -gt $max ]]; then
+ log_error "Security sensitive function ${FUNCNAME[1]} should have had between $min and $max parameters but had $# instead."
+ log_error "Security sensitive function ${FUNCNAME[1]} was called from ${BASH_SOURCE[2]}:${BASH_LINENO[1]} ${FUNCNAME[2]} with these parameters: $*"
+ log_error "This should be reported as a bug."
+ return 1
+ fi
+ return 0
+}
diff --git a/lib/modules.sh b/lib/modules.sh
new file mode 100644
index 0000000..10428e6
--- /dev/null
+++ b/lib/modules.sh
@@ -0,0 +1,447 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Modules management
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## List of loaded modules. Don't change from other code.
+## @Type Semi-private
+#---------------------------------------------------------------------
+modules_loaded=""
+
+#---------------------------------------------------------------------
+## Current module API version.
+#---------------------------------------------------------------------
+declare -r modules_current_API=2
+
+
+#---------------------------------------------------------------------
+## Call from after_load with a list of modules that you depend on
+## @Type API
+## @param What module you are calling from.
+## @param Space separated list of modules you depend on
+## @return 0 Success
+## @return 1 Other error. You should return 1 from after_load.
+## @return 2 One or several of the dependencies could found. You should return 1 from after_load.
+## @return 3 Not all of the dependencies could be loaded (modules exist but did not load correctly). You should return 1 from after_load.
+#---------------------------------------------------------------------
+modules_depends_register() {
+ local callermodule="$1"
+ local dep
+ for dep in $2; do
+ if [[ $dep == $callermodule ]]; then
+ log_error_file modules.log "To the module author of $callermodule: You can't list yourself as a dependency of yourself!"
+ log_error_file modules.log "Aborting!"
+ return 1
+ fi
+ if ! list_contains "modules_loaded" "$dep"; then
+ log_info_file modules.log "Loading dependency of $callermodule: $dep"
+ modules_load "$dep"
+ local status="$?"
+ if [[ $status -eq 4 ]]; then
+ return 2
+ elif [[ $status -ne 0 ]]; then
+ return 3
+ fi
+ fi
+ if list_contains "modules_depends_${dep}" "$callermodule"; then
+ log_warning_file modules.log "Dependency ${callermodule} already listed as depending on ${dep}!?"
+ fi
+ # Use printf not eval here.
+ local listname="modules_depends_${dep}"
+ printf -v "modules_depends_${dep}" '%s' "${!listname} $callermodule"
+ done
+}
+
+#---------------------------------------------------------------------
+## Call from after_load or INIT with a list of modules that you
+## depend on optionally.
+## @Type API
+## @param What module you are calling from.
+## @param The module you want to depend on optionally.
+## @return 0 Success, module loaded
+## @return 1 User didn't list it as loaded, don't use the features in question
+## @return 2 Other error. You should return 1 from after_load.
+## @return 3 One or several of the dependencies could found. You should return 1 from after_load.
+## @return 4 Not all of the dependencies could be loaded (modules exist but did not load correctly). You should return 1 from after_load.
+#---------------------------------------------------------------------
+modules_depends_register_optional() {
+ local callermodule="$1"
+ local dep="$2"
+ if ! list_contains "modules_loaded" "$dep"; then
+ # So not loaded, now we need to find out if we should load it or not
+ # We use $config_modules for it
+ if ! list_contains 'config_modules' "$dep"; then
+ log_info_file modules.log "Optional dependency of $callermodule ($dep) not loaded."
+ return 1
+ fi
+ log_info_file modules.log "Loading optional dependency of $callermodule: ($dep)"
+ fi
+ # Ah we should load it then? Call modules_depends_register
+ modules_depends_register "$@"
+}
+
+
+#---------------------------------------------------------------------
+## Semi internal!
+## List modules that depend on another module.
+## @Type Semi-private
+## @param Module to check
+## @Stdout List of modules that depend on this.
+#---------------------------------------------------------------------
+modules_depends_list_deps() {
+ # This is needed to be able to use indirect refs
+ local deplistname="modules_depends_${1}"
+ # Clean out spaces, fastest way
+ echo ${!deplistname}
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+# See doc/module_api.txt instead #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Used by unload to unregister from depends system
+## (That is: remove from list of "depended on by" of other modules)
+## @Type Private
+## @param Module to unregister
+#---------------------------------------------------------------------
+modules_depends_unregister() {
+ local module newval
+ for module in $modules_loaded; do
+ if list_contains "modules_depends_${module}" "$1"; then
+ list_remove "modules_depends_${module}" "$1" "modules_depends_${module}"
+ fi
+ done
+}
+
+#---------------------------------------------------------------------
+## Check if a module can be unloaded
+## @Type Private
+## @param Name of module to check
+## @return Can be unloaded
+## @return Is needed by some other module.
+#---------------------------------------------------------------------
+modules_depends_can_unload() {
+ # This is needed to be able to use indirect refs
+ local deplistname="modules_depends_${1}"
+ # Not empty/only whitespaces?
+ if ! [[ ${!deplistname} =~ ^\ *$ ]]; then
+ return 1
+ fi
+ return 0
+}
+
+#---------------------------------------------------------------------
+## Add hooks for a module
+## @Type Private
+## @param Module name
+## @param MODULE_BASE_PATH, exported to INIT as a part of the API
+## @return 0 Success
+## @return 1 module_modulename_INIT returned non-zero
+## @return 2 Module wanted to register an unknown hook.
+#---------------------------------------------------------------------
+modules_add_hooks() {
+ local module="$1"
+ local modinit_HOOKS
+ local modinit_API
+ local MODULE_BASE_PATH="$2"
+ module_${module}_INIT "$module"
+ [[ $? -ne 0 ]] && { log_error_file modules.log "Failed to get initialize module \"$module\""; return 1; }
+ # Check if it didn't set any modinit_API, in that case it is a API 1 module.
+ if [[ -z $modinit_API ]]; then
+ log_error "Please upgrade \"$module\" to new module API $modules_current_API. This old API is obsolete and no longer supported."
+ return 1
+ elif [[ $modinit_API -ne $modules_current_API ]]; then
+ log_error "Current module API version is $modules_current_API, but the API version of \"$module\" is $module_API."
+ return 1
+ fi
+
+ local hook
+ for hook in $modinit_HOOKS; do
+ case $hook in
+ "FINALISE")
+ modules_FINALISE+=" $module"
+ ;;
+ "after_load")
+ modules_after_load+=" $module"
+ ;;
+ "before_connect")
+ modules_before_connect+=" $module"
+ ;;
+ "on_connect")
+ modules_on_connect+=" $module"
+ ;;
+ "after_connect")
+ modules_after_connect+=" $module"
+ ;;
+ "before_disconnect")
+ modules_before_disconnect+=" $module"
+ ;;
+ "after_disconnect")
+ modules_after_disconnect+=" $module"
+ ;;
+ "on_module_UNLOAD")
+ modules_on_module_UNLOAD+=" $module"
+ ;;
+ "on_server_ERROR")
+ modules_on_server_ERROR+=" $module"
+ ;;
+ "on_NOTICE")
+ modules_on_NOTICE+=" $module"
+ ;;
+ "on_PRIVMSG")
+ modules_on_PRIVMSG+=" $module"
+ ;;
+ "on_TOPIC")
+ modules_on_TOPIC+=" $module"
+ ;;
+ "on_channel_MODE")
+ modules_on_channel_MODE+=" $module"
+ ;;
+ "on_user_MODE")
+ modules_on_user_MODE+=" $module"
+ ;;
+ "on_INVITE")
+ modules_on_INVITE+=" $module"
+ ;;
+ "on_JOIN")
+ modules_on_JOIN+=" $module"
+ ;;
+ "on_PART")
+ modules_on_PART+=" $module"
+ ;;
+ "on_KICK")
+ modules_on_KICK+=" $module"
+ ;;
+ "on_QUIT")
+ modules_on_QUIT+=" $module"
+ ;;
+ "on_KILL")
+ modules_on_KILL+=" $module"
+ ;;
+ "on_NICK")
+ modules_on_NICK+=" $module"
+ ;;
+ "on_numeric")
+ modules_on_numeric+=" $module"
+ ;;
+ "on_PONG")
+ modules_on_PONG+=" $module"
+ ;;
+ "on_raw")
+ modules_on_raw+=" $module"
+ ;;
+ *)
+ log_error_file modules.log "Unknown hook $hook requested. Module may malfunction. Module will be unloaded"
+ return 2
+ ;;
+ esac
+ done
+}
+
+#---------------------------------------------------------------------
+## List of all the optional hooks.
+## @Type Private
+#---------------------------------------------------------------------
+modules_hooks="FINALISE after_load before_connect on_connect after_connect before_disconnect after_disconnect on_module_UNLOAD on_server_ERROR on_NOTICE on_PRIVMSG on_TOPIC on_channel_MODE on_user_MODE on_INVITE on_JOIN on_PART on_KICK on_QUIT on_KILL on_NICK on_numeric on_PONG on_raw"
+
+#---------------------------------------------------------------------
+## Unload a module
+## @Type Private
+## @param Module name
+## @return 0 Unloaded
+## @return 2 Module not loaded
+## @return 3 Can't unload, some other module depends on this.
+## @Note If the unload fails for other reasons the bot will quit.
+#---------------------------------------------------------------------
+modules_unload() {
+ local module="$1"
+ local hook newval to_unset
+ if ! list_contains "modules_loaded" "$module"; then
+ log_warning_file modules.log "No such module as $1 is loaded."
+ return 2
+ fi
+ if ! modules_depends_can_unload "$module"; then
+ log_error_file modules.log "Can't unload $module because these module(s) depend(s) on it: $(modules_depends_list_deps "$module")"
+ return 3
+ fi
+
+ # Remove hooks from list first in case unloading fails so we can do quit hooks if something break.
+ for hook in $modules_hooks; do
+ # List so we can unset.
+ if list_contains "modules_${hook}" "$module"; then
+ to_unset+=" module_${module}_${hook}"
+ fi
+ list_remove "modules_${hook}" "$module" "modules_${hook}"
+ done
+ commands_unregister "$module" || {
+ log_fatal_file modules.log "Could not unregister commands for ${module}"
+ bot_quit "Fatal error in module unload, please see log"
+ }
+ module_${module}_UNLOAD || {
+ log_fatal_file modules.log "Could not unload ${module}, module_${module}_UNLOAD returned ${?}!"
+ bot_quit "Fatal error in module unload, please see log"
+ }
+ unset module_${module}_UNLOAD
+ unset module_${module}_INIT
+ unset module_${module}_REHASH
+ # Unset from list created above.
+ for hook in $to_unset; do
+ unset "$hook" || {
+ log_fatal_file modules.log "Could not unset the hook $hook of module $module!"
+ bot_quit "Fatal error in module unload, please see log"
+ }
+ done
+ modules_depends_unregister "$module"
+ list_remove "modules_loaded" "$module" "modules_loaded"
+
+ # Call any hooks for unloading modules.
+ local othermodule
+ for othermodule in $modules_on_module_UNLOAD; do
+ module_${othermodule}_on_module_UNLOAD "$module"
+ done
+
+ # Unset help string
+ unset helpentry_module_${module}_description
+
+ return 0
+}
+
+#---------------------------------------------------------------------
+## Generate awk script to validate module functions.
+## @param Module name
+## @Type Private
+## @return 0 If the file is OK
+## @return 1 If the file lacks one of more of the functions.
+#---------------------------------------------------------------------
+modules_check_function() {
+ local module="$1"
+ # This is a one liner. Well mostly. ;)
+ # We check that the needed functions exist.
+ awk "function check_found() { if (init && unload && rehash) exit 0 }
+ /^declare -f module_${module}_INIT$/ { init=1; check_found() }
+ /^declare -f module_${module}_UNLOAD$/ { unload=1; check_found() }
+ /^declare -f module_${module}_REHASH$/ { rehash=1; check_found() }
+ END { if (! (init && unload && rehash)) exit 1 }"
+}
+
+#---------------------------------------------------------------------
+## Load a module
+## @Type Private
+## @param Name of module to load
+## @return 0 Loaded Ok
+## @return 1 Other errors
+## @return 2 Module already loaded
+## @return 3 Failed to source it in safe subshell
+## @return 4 Failed to source it
+## @return 5 No such module
+## @return 6 Getting hooks failed
+## @return 7 after_load failed
+## @Note If the load fails in a fatal way the bot will quit.
+#---------------------------------------------------------------------
+modules_load() {
+ local module="$1"
+ if list_contains "modules_loaded" "$module"; then
+ log_warning_file modules.log "Module ${module} is already loaded."
+ return 2
+ fi
+ # modulebase is exported as MODULE_BASE_PATH
+ # with ${config_modules_dir} prepended to the
+ # INIT function, useful for multi-file
+ # modules, but available for other modules too.
+ local modulefilename modulebase
+ if [[ -f "${config_modules_dir}/m_${module}.sh" ]]; then
+ modulefilename="m_${module}.sh"
+ modulebase="${modulefilename}"
+ elif [[ -d "${config_modules_dir}/m_${module}" && -f "${config_modules_dir}/m_${module}/__main__.sh" ]]; then
+ modulefilename="m_${module}/__main__.sh"
+ modulebase="m_${module}"
+ else
+ log_error_file modules.log "No such module as ${module} exists."
+ return 5
+ fi
+ ( source "${config_modules_dir}/${modulefilename}" )
+ if [[ $? -ne 0 ]]; then
+ log_error_file modules.log "Could not load ${module}, failed to source it in safe subshell."
+ return 3
+ fi
+ ( source "${config_modules_dir}/${modulefilename}" && declare -F ) | modules_check_function "$module"
+ if [[ $? -ne 0 ]]; then
+ log_error_file modules.log "Could not load ${module}, it lacks some important functions it should have."
+ return 3
+ fi
+ source "${config_modules_dir}/${modulefilename}"
+ if [[ $? -eq 0 ]]; then
+ modules_loaded+=" $module"
+ modules_add_hooks "$module" "${config_modules_dir}/${modulebase}" || \
+ {
+ log_error_file modules.log "Hooks failed for $module"
+ # Try to unload.
+ modules_unload "$module" || {
+ log_fatal_file modules.log "Failed Unloading of $module (that failed to load)."
+ bot_quit "Fatal error in module unload of failed module load, please see log"
+ }
+ return 6
+ }
+ if grep -qw "$module" <<< "$modules_after_load"; then
+ module_${module}_after_load
+ if [[ $? -ne 0 ]]; then
+ modules_unload ${module} || {
+ log_fatal_file modules.log "Unloading of $module that failed after_load failed."
+ bot_quit "Fatal error in module unload of failed module load (after_load), please see log"
+ }
+ return 7
+ fi
+ fi
+ else
+ log_error_file modules.log "Could not load ${module}, failed to source it."
+ return 4
+ fi
+}
+
+#---------------------------------------------------------------------
+## Load modules from the config
+## @Type Private
+#---------------------------------------------------------------------
+modules_load_from_config() {
+ local module
+ IFS=" "
+ for module in $modules_loaded; do
+ if ! list_contains config_modules "$module"; then
+ modules_unload "$module"
+ fi
+ done
+ unset IFS
+ for module in $config_modules; do
+ if [[ -f "${config_modules_dir}/m_${module}.sh" || -d "${config_modules_dir}/m_${module}" ]]; then
+ if ! list_contains modules_loaded "$module"; then
+ modules_load "$module"
+ fi
+ else
+ log_warning_file modules.log "$module doesn't exist! Removing it from list"
+ fi
+ done
+}
diff --git a/lib/numerics.sh b/lib/numerics.sh
new file mode 100644
index 0000000..bfe3d33
--- /dev/null
+++ b/lib/numerics.sh
@@ -0,0 +1,348 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+
+###########################################################################
+# #
+# WARNING THIS FILE IS AUTOGENERATED. ANY CHANGES WILL BE OVERWRITTEN! #
+# See the source in tools/numerics.txt for comments about some numerics #
+# This file was generated with tools/build_numerics.sh #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Auto-generated list of numerics from tools/numerics.txt<br />
+## This file contains a list of numerics that we currently use.
+## It is therefore incomplete.<br />
+## Because the list of variables in this file is so long, please see
+## it's source for more details.
+#---------------------------------------------------------------------
+
+##########################
+# Name -> number mapping #
+##########################
+
+numeric_RPL_WELCOME='001'
+numeric_RPL_YOURHOST='002'
+numeric_RPL_CREATED='003'
+numeric_RPL_MYINFO='004'
+numeric_RPL_ISUPPORT='005'
+numeric_RPL_MAP='006'
+numeric_RPL_MAPEND='007'
+numeric_RPL_SNOMASK='008'
+numeric_RPL_TRACEUSER='205'
+numeric_RPL_STATSCLINE='213'
+numeric_RPL_ENDOFSTATS='219'
+numeric_RPL_UMODEIS='221'
+numeric_RPL_STATSELINE='223'
+numeric_RPL_RULES='232'
+numeric_RPL_STATSUPTIME='242'
+numeric_RPL_STATSCONN='250'
+numeric_RPL_LUSERCLIENT='251'
+numeric_RPL_LUSEROP='252'
+numeric_RPL_LUSERUNKNOWN='253'
+numeric_RPL_LUSERCHANNELS='254'
+numeric_RPL_LUSERME='255'
+numeric_RPL_ADMINME='256'
+numeric_RPL_ADMINLOC1='257'
+numeric_RPL_ADMINLOC2='258'
+numeric_RPL_ADMINEMAIL='259'
+numeric_RPL_TRYAGAIN='263'
+numeric_RPL_LOCALUSERS='265'
+numeric_RPL_GLOBALUSERS='266'
+numeric_RPL_SILELIST='271'
+numeric_RPL_ENDOFSILELIST='272'
+numeric_RPL_AWAY='301'
+numeric_RPL_USERHOST='302'
+numeric_RPL_ISON='303'
+numeric_RPL_TEXT='304'
+numeric_RPL_UNAWAY='305'
+numeric_RPL_UNAWAY='306'
+numeric_RPL_WHOISREGNICK='307'
+numeric_RPL_RULESSTART='308'
+numeric_RPL_ENDOFRULES='309'
+numeric_RPL_WHOISHELPOP='310'
+numeric_RPL_WHOISUSER='311'
+numeric_RPL_WHOISSERVER='312'
+numeric_RPL_WHOISOPERATOR='313'
+numeric_RPL_WHOWASUSER='314'
+numeric_RPL_ENDOFWHO='315'
+numeric_RPL_WHOISIDLE='317'
+numeric_RPL_ENDOFWHOIS='318'
+numeric_RPL_WHOISCHANNELS='319'
+numeric_RPL_WHOISSPECIAL='320'
+numeric_RPL_LISTSTART='321'
+numeric_RPL_LIST='322'
+numeric_RPL_LISTEND='323'
+numeric_RPL_CHANNELMODEIS='324'
+numeric_RPL_CREATIONTIME='329'
+numeric_RPL_WHOISACCOUNT='330'
+numeric_RPL_NOTOPIC='331'
+numeric_RPL_TOPIC='332'
+numeric_RPL_TOPICWHOTIME='333'
+numeric_RPL_USERIP='340'
+numeric_RPL_INVITING='341'
+numeric_RPL_INVITELIST='346'
+numeric_RPL_ENDOFINVITELIST='347'
+numeric_RPL_EXCEPTLIST='348'
+numeric_RPL_ENDOFEXCEPTLIST='349'
+numeric_RPL_VERSION='351'
+numeric_RPL_WHOREPLY='352'
+numeric_RPL_NAMREPLY='353'
+numeric_RPL_LINKS='364'
+numeric_RPL_ENDOFLINKS='365'
+numeric_RPL_ENDOFNAMES='366'
+numeric_RPL_BANLIST='367'
+numeric_RPL_ENDOFBANLIST='368'
+numeric_RPL_ENDOFWHOWAS='369'
+numeric_RPL_INFO='371'
+numeric_RPL_MOTD='372'
+numeric_RPL_ENDOFINFO='374'
+numeric_RPL_MOTDSTART='375'
+numeric_RPL_ENDOFMOTD='376'
+numeric_RPL_WHOISHOST='378'
+numeric_RPL_YOUREOPER='381'
+numeric_RPL_REHASHING='382'
+numeric_RPL_TIME='391'
+numeric_RPL_HOSTHIDDEN='396'
+numeric_ERR_NOSUCHNICK='401'
+numeric_ERR_NOSUCHSERVER='402'
+numeric_ERR_NOSUCHCHANNEL='403'
+numeric_ERR_CANNOTSENDTOCHAN='404'
+numeric_ERR_TOOMANYCHANNELS='405'
+numeric_ERR_WASNOSUCHNICK='406'
+numeric_ERR_TOOMANYTARGETS='407'
+numeric_ERR_NOTEXTTOSEND='412'
+numeric_ERR_TOOMANYMATCHES='416'
+numeric_ERR_UNKNOWNCOMMAND='421'
+numeric_ERR_NOMOTD='422'
+numeric_ERR_ERRONEUSNICKNAME='432'
+numeric_ERR_NICKNAMEINUSE='433'
+numeric_ERR_NICKTOOFAST='438'
+numeric_ERR_USERNOTINCHANNEL='441'
+numeric_ERR_NOTONCHANNEL='442'
+numeric_ERR_USERONCHANNEL='443'
+numeric_ERR_SUMMONDISABLED='445'
+numeric_ERR_USERSDISABLED='446'
+numeric_ERR_NONICKCHANGE='447'
+numeric_ERR_NOTFORHALFOPS='460'
+numeric_ERR_NEEDMOREPARAMS='461'
+numeric_ERR_ALREADYREGISTERED='462'
+numeric_ERR_ONLYSERVERSCANCHANGE='468'
+numeric_ERR_LINKCHANNEL='470'
+numeric_ERR_CHANNELISFULL='471'
+numeric_ERR_UNKNOWNMODE='472'
+numeric_ERR_INVITEONLYCHAN='473'
+numeric_ERR_BANNEDFROMCHAN='474'
+numeric_ERR_BADCHANNELKEY='475'
+numeric_ERR_NEEDREGGEDNICK='477'
+numeric_ERR_BANLISTFULL='478'
+numeric_ERR_CANNOTKNOCK='480'
+numeric_ERR_NOPRIVILEGES='481'
+numeric_ERR_CHANOPRIVSNEEDED='482'
+numeric_ERR_ATTACKDENY='484'
+numeric_ERR_SECUREONLYCHAN='489'
+numeric_ERR_ALLMUSTUSESSL='490'
+numeric_ERR_NOOPERHOST='491'
+numeric_ERR_NOREJOINONKICK='495'
+numeric_ERR_CHANOWNPRIVNEEDED='499'
+numeric_ERR_UMODEUNKNOWNFLAG='501'
+numeric_ERR_USERSDONTMATCH='502'
+numeric_RPL_LOGON='600'
+numeric_RPL_LOGOFF='601'
+numeric_RPL_WATCHOFF='602'
+numeric_RPL_NOWON='604'
+numeric_RPL_NOWOFF='605'
+numeric_RPL_WATCHLIST='606'
+numeric_RPL_ENDOFWATCHLIST='607'
+numeric_RPL_WHOISSECURE='671'
+numeric_RPL_MODULES='900'
+numeric_RPL_ENDOFMODULES='901'
+numeric_RPL_COMMANDS='902'
+numeric_RPL_ENDOFCOMMANDS='903'
+numeric_ERR_CENSORED='936'
+numeric_ERR_ALREDYCENSORED='937'
+numeric_ERR_NOTCENSORED='938'
+numeric_ERR_SPAMFILTERLISTFULL='939'
+numeric_RPL_ENDOFSPAMFILTER='940'
+numeric_RPL_SPAMFILTER='941'
+numeric_ERR_INVALIDNICK='942'
+numeric_RPL_SILENCEREMOVED='950'
+numeric_RPL_SILENCEADDED='951'
+numeric_ERR_ALREADYSILENCE='952'
+numeric_ERR_CANNOTDOCOMMAND='972'
+numeric_ERR_CANNOTCHANGECHANMODE='974'
+
+##########################
+# Number -> name mapping #
+##########################
+
+numerics[1]='RPL_WELCOME'
+numerics[2]='RPL_YOURHOST'
+numerics[3]='RPL_CREATED'
+numerics[4]='RPL_MYINFO'
+numerics[5]='RPL_ISUPPORT'
+numerics[6]='RPL_MAP'
+numerics[7]='RPL_MAPEND'
+numerics[8]='RPL_SNOMASK'
+numerics[205]='RPL_TRACEUSER'
+numerics[213]='RPL_STATSCLINE'
+numerics[219]='RPL_ENDOFSTATS'
+numerics[221]='RPL_UMODEIS'
+numerics[223]='RPL_STATSELINE'
+numerics[232]='RPL_RULES'
+numerics[242]='RPL_STATSUPTIME'
+numerics[250]='RPL_STATSCONN'
+numerics[251]='RPL_LUSERCLIENT'
+numerics[252]='RPL_LUSEROP'
+numerics[253]='RPL_LUSERUNKNOWN'
+numerics[254]='RPL_LUSERCHANNELS'
+numerics[255]='RPL_LUSERME'
+numerics[256]='RPL_ADMINME'
+numerics[257]='RPL_ADMINLOC1'
+numerics[258]='RPL_ADMINLOC2'
+numerics[259]='RPL_ADMINEMAIL'
+numerics[263]='RPL_TRYAGAIN'
+numerics[265]='RPL_LOCALUSERS'
+numerics[266]='RPL_GLOBALUSERS'
+numerics[271]='RPL_SILELIST'
+numerics[272]='RPL_ENDOFSILELIST'
+numerics[301]='RPL_AWAY'
+numerics[302]='RPL_USERHOST'
+numerics[303]='RPL_ISON'
+numerics[304]='RPL_TEXT'
+numerics[305]='RPL_UNAWAY'
+numerics[306]='RPL_UNAWAY'
+numerics[307]='RPL_WHOISREGNICK'
+numerics[308]='RPL_RULESSTART'
+numerics[309]='RPL_ENDOFRULES'
+numerics[310]='RPL_WHOISHELPOP'
+numerics[311]='RPL_WHOISUSER'
+numerics[312]='RPL_WHOISSERVER'
+numerics[313]='RPL_WHOISOPERATOR'
+numerics[314]='RPL_WHOWASUSER'
+numerics[315]='RPL_ENDOFWHO'
+numerics[317]='RPL_WHOISIDLE'
+numerics[318]='RPL_ENDOFWHOIS'
+numerics[319]='RPL_WHOISCHANNELS'
+numerics[320]='RPL_WHOISSPECIAL'
+numerics[321]='RPL_LISTSTART'
+numerics[322]='RPL_LIST'
+numerics[323]='RPL_LISTEND'
+numerics[324]='RPL_CHANNELMODEIS'
+numerics[329]='RPL_CREATIONTIME'
+numerics[330]='RPL_WHOISACCOUNT'
+numerics[331]='RPL_NOTOPIC'
+numerics[332]='RPL_TOPIC'
+numerics[333]='RPL_TOPICWHOTIME'
+numerics[340]='RPL_USERIP'
+numerics[341]='RPL_INVITING'
+numerics[346]='RPL_INVITELIST'
+numerics[347]='RPL_ENDOFINVITELIST'
+numerics[348]='RPL_EXCEPTLIST'
+numerics[349]='RPL_ENDOFEXCEPTLIST'
+numerics[351]='RPL_VERSION'
+numerics[352]='RPL_WHOREPLY'
+numerics[353]='RPL_NAMREPLY'
+numerics[364]='RPL_LINKS'
+numerics[365]='RPL_ENDOFLINKS'
+numerics[366]='RPL_ENDOFNAMES'
+numerics[367]='RPL_BANLIST'
+numerics[368]='RPL_ENDOFBANLIST'
+numerics[369]='RPL_ENDOFWHOWAS'
+numerics[371]='RPL_INFO'
+numerics[372]='RPL_MOTD'
+numerics[374]='RPL_ENDOFINFO'
+numerics[375]='RPL_MOTDSTART'
+numerics[376]='RPL_ENDOFMOTD'
+numerics[378]='RPL_WHOISHOST'
+numerics[381]='RPL_YOUREOPER'
+numerics[382]='RPL_REHASHING'
+numerics[391]='RPL_TIME'
+numerics[396]='RPL_HOSTHIDDEN'
+numerics[401]='ERR_NOSUCHNICK'
+numerics[402]='ERR_NOSUCHSERVER'
+numerics[403]='ERR_NOSUCHCHANNEL'
+numerics[404]='ERR_CANNOTSENDTOCHAN'
+numerics[405]='ERR_TOOMANYCHANNELS'
+numerics[406]='ERR_WASNOSUCHNICK'
+numerics[407]='ERR_TOOMANYTARGETS'
+numerics[412]='ERR_NOTEXTTOSEND'
+numerics[416]='ERR_TOOMANYMATCHES'
+numerics[421]='ERR_UNKNOWNCOMMAND'
+numerics[422]='ERR_NOMOTD'
+numerics[432]='ERR_ERRONEUSNICKNAME'
+numerics[433]='ERR_NICKNAMEINUSE'
+numerics[438]='ERR_NICKTOOFAST'
+numerics[441]='ERR_USERNOTINCHANNEL'
+numerics[442]='ERR_NOTONCHANNEL'
+numerics[443]='ERR_USERONCHANNEL'
+numerics[445]='ERR_SUMMONDISABLED'
+numerics[446]='ERR_USERSDISABLED'
+numerics[447]='ERR_NONICKCHANGE'
+numerics[460]='ERR_NOTFORHALFOPS'
+numerics[461]='ERR_NEEDMOREPARAMS'
+numerics[462]='ERR_ALREADYREGISTERED'
+numerics[468]='ERR_ONLYSERVERSCANCHANGE'
+numerics[470]='ERR_LINKCHANNEL'
+numerics[471]='ERR_CHANNELISFULL'
+numerics[472]='ERR_UNKNOWNMODE'
+numerics[473]='ERR_INVITEONLYCHAN'
+numerics[474]='ERR_BANNEDFROMCHAN'
+numerics[475]='ERR_BADCHANNELKEY'
+numerics[477]='ERR_NEEDREGGEDNICK'
+numerics[478]='ERR_BANLISTFULL'
+numerics[480]='ERR_CANNOTKNOCK'
+numerics[481]='ERR_NOPRIVILEGES'
+numerics[482]='ERR_CHANOPRIVSNEEDED'
+numerics[484]='ERR_ATTACKDENY'
+numerics[489]='ERR_SECUREONLYCHAN'
+numerics[490]='ERR_ALLMUSTUSESSL'
+numerics[491]='ERR_NOOPERHOST'
+numerics[495]='ERR_NOREJOINONKICK'
+numerics[499]='ERR_CHANOWNPRIVNEEDED'
+numerics[501]='ERR_UMODEUNKNOWNFLAG'
+numerics[502]='ERR_USERSDONTMATCH'
+numerics[600]='RPL_LOGON'
+numerics[601]='RPL_LOGOFF'
+numerics[602]='RPL_WATCHOFF'
+numerics[604]='RPL_NOWON'
+numerics[605]='RPL_NOWOFF'
+numerics[606]='RPL_WATCHLIST'
+numerics[607]='RPL_ENDOFWATCHLIST'
+numerics[671]='RPL_WHOISSECURE'
+numerics[900]='RPL_MODULES'
+numerics[901]='RPL_ENDOFMODULES'
+numerics[902]='RPL_COMMANDS'
+numerics[903]='RPL_ENDOFCOMMANDS'
+numerics[936]='ERR_CENSORED'
+numerics[937]='ERR_ALREDYCENSORED'
+numerics[938]='ERR_NOTCENSORED'
+numerics[939]='ERR_SPAMFILTERLISTFULL'
+numerics[940]='RPL_ENDOFSPAMFILTER'
+numerics[941]='RPL_SPAMFILTER'
+numerics[942]='ERR_INVALIDNICK'
+numerics[950]='RPL_SILENCEREMOVED'
+numerics[951]='RPL_SILENCEADDED'
+numerics[952]='ERR_ALREADYSILENCE'
+numerics[972]='ERR_CANNOTDOCOMMAND'
+numerics[974]='ERR_CANNOTCHANGECHANMODE'
+
+# End of generated file.
diff --git a/lib/parse.sh b/lib/parse.sh
new file mode 100644
index 0000000..6abb9ab
--- /dev/null
+++ b/lib/parse.sh
@@ -0,0 +1,100 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Data parsing
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Get parts of hostmask.
+## @Note In most cases you should use one of
+## @Note <@function parse_hostmask_nick>, <@function parse_hostmask_ident>
+## @Note or <@function parse_hostmask_host>. Only use this function
+## @Note if you want all several parts.
+## @Type API
+## @param n!u@h mask
+## @param Variable to return nick in
+## @param Variable to return ident in
+## @param Variable to return host in
+#---------------------------------------------------------------------
+parse_hostmask() {
+ if [[ $1 =~ ^([^ !]+)!([^ @]+)@([^ ]+) ]]; then
+ printf -v "$2" '%s' "${BASH_REMATCH[1]}"
+ printf -v "$3" '%s' "${BASH_REMATCH[2]}"
+ printf -v "$4" '%s' "${BASH_REMATCH[3]}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Get nick from hostmask
+## @Type API
+## @param n!u@h mask
+## @param Variable to return result in
+#---------------------------------------------------------------------
+parse_hostmask_nick() {
+ if [[ $1 =~ ^([^ !]+)! ]]; then
+ printf -v "$2" '%s' "${BASH_REMATCH[1]}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Get ident from hostmask
+## @Type API
+## @param n!u@h mask
+## @param Variable to return result in
+#---------------------------------------------------------------------
+parse_hostmask_ident() {
+ if [[ $1 =~ ^[^\ !]+!([^ @]+)@ ]]; then
+ printf -v "$2" '%s' "${BASH_REMATCH[1]}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Get host from hostmask
+## @Type API
+## @param n!u@h mask
+## @param Variable to return result in
+#---------------------------------------------------------------------
+parse_hostmask_host() {
+ if [[ $1 =~ ^[^\ !]+![^\ @]+@([^ ]+) ]]; then
+ printf -v "$2" '%s' "${BASH_REMATCH[1]}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## This is used to get data out of 005.
+## @Type API
+## @param Name of data to get
+## @param Variable to return result (if any result) in
+## @return 0 If found otherwise 1
+## @Note That if the variable doesn't have any data,
+## @Note but still exist it will return nothing on STDOUT
+## @Note but 0 as error code
+#---------------------------------------------------------------------
+parse_005() {
+ if [[ $server_005 =~ ${1}(=([^ ]+))? ]]; then
+ if [[ ${BASH_REMATCH[2]} ]]; then
+ printf -v "$2" '%s' "${BASH_REMATCH[2]}"
+ fi
+ return 0
+ fi
+ return 1
+}
diff --git a/lib/send.sh b/lib/send.sh
new file mode 100644
index 0000000..2b3d488
--- /dev/null
+++ b/lib/send.sh
@@ -0,0 +1,178 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Functions for sending data to server
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Simple flood limiting.
+## Note that this doesn't handle this very well:<br />
+## seconds:milliseconds message<br />
+## 01:999 message<br />
+## 02:001 other message<br />
+## Then they get too close.<br />
+## I think this won't flood us off though.<br />
+## @Type Private
+#---------------------------------------------------------------------
+send_last=0
+
+#---------------------------------------------------------------------
+## Send a "raw" line to the server.
+## @Type API
+## @param Line to send
+#---------------------------------------------------------------------
+send_raw() {
+ # Do the flood limiting
+ local now=
+ time_get_current 'now'
+ if [[ "$send_last" == "$now" ]]; then
+ sleep 1
+ fi
+ time_get_current 'send_last'
+ send_raw_flood "$*"
+}
+
+#---------------------------------------------------------------------
+## Send a PRIVMSG
+## @Type API
+## @param Who (channel or nick)
+## @param Message
+#---------------------------------------------------------------------
+send_msg() {
+ # Don't do anything if no message
+ [[ -z $2 ]] && return 0
+ send_raw "PRIVMSG ${1} :${2}"
+}
+
+#---------------------------------------------------------------------
+## Send a NOTICE
+## @Type API
+## @param Who (channel or nick)
+## @param Message
+#---------------------------------------------------------------------
+send_notice() {
+ # Don't do anything if no message
+ [[ -z $2 ]] && return 0
+ send_raw "NOTICE ${1} :${2}"
+}
+
+#---------------------------------------------------------------------
+## Send a CTCP
+## @Type API
+## @param Who (channel or nick)
+## @param Message
+#---------------------------------------------------------------------
+send_ctcp() {
+ # Don't do anything if no message
+ [[ -z $2 ]] && return 0
+ send_msg "$1" $'\1'"${2}"$'\1'
+}
+
+#---------------------------------------------------------------------
+## Send a NCTCP (ctcp reply)
+## @Type API
+## @param Who (channel or nick)
+## @param Message
+#---------------------------------------------------------------------
+send_nctcp() {
+ # Don't do anything if no message
+ [[ -z $2 ]] && return 0
+ send_notice "$1" $'\1'"${2}"$'\1'
+}
+
+#---------------------------------------------------------------------
+## Send a NICK to change nick
+## @Type API
+## @param New nick
+#---------------------------------------------------------------------
+send_nick() {
+ send_raw "NICK ${1}"
+}
+
+#---------------------------------------------------------------------
+## Send a MODE to change umodes.
+## @Type API
+## @param Modes to send
+#---------------------------------------------------------------------
+send_umodes() {
+ send_raw "MODE $server_nick_current $1"
+}
+
+#---------------------------------------------------------------------
+## Send a MODE to change channel modes.
+## @Type API
+## @param Target channel
+## @param Modes to set
+#---------------------------------------------------------------------
+send_modes() {
+ send_raw "MODE $1 $2"
+}
+
+#---------------------------------------------------------------------
+## Send a TOPIC to change channel topic.
+## @Type API
+## @param Channel to change topic of
+## @param New topic.
+#---------------------------------------------------------------------
+send_topic() {
+ send_raw "TOPIC $1 :$2"
+}
+
+#---------------------------------------------------------------------
+## This is semi-internal only
+## This may flood ourself off. Use send_raw instead in most cases.
+## Also this doesn't log the actual line, so used for passwords.
+## @Type API
+## @param What to log instead (example could be: "NickServ IDENTIFY (password)")
+## @param The line to send
+#---------------------------------------------------------------------
+send_raw_flood_nolog() {
+ log_raw_out "<hidden line from logs>: $1"
+ transport_write_line "$2"$'\r'
+}
+
+#---------------------------------------------------------------------
+## This is semi-internal only
+## This may flood ourself off. Use send_raw instead in most cases.
+## Same syntax as send_raw
+## @Type Semi-private
+#---------------------------------------------------------------------
+send_raw_flood() {
+ log_raw_out "$*"
+ transport_write_line "$*"$'\r'
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Module authors: use the wrapper: bot_quit in misc.sh instead!
+## @Type Private
+## @param If set, a quit reason
+#---------------------------------------------------------------------
+send_quit() {
+ local reason=""
+ [[ -n "$1" ]] && reason=" :$1"
+ send_raw_flood "QUIT${reason}"
+}
diff --git a/lib/server.sh b/lib/server.sh
new file mode 100644
index 0000000..348cb73
--- /dev/null
+++ b/lib/server.sh
@@ -0,0 +1,337 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Server connection.
+#---------------------------------------------------------------------
+
+# Server info variables
+#---------------------------------------------------------------------
+## Name of server (example: server1.example.net)
+## @Type API
+#---------------------------------------------------------------------
+server_name=""
+#---------------------------------------------------------------------
+## The 004 received from the server.
+## @Type API
+#---------------------------------------------------------------------
+server_004=""
+#---------------------------------------------------------------------
+## The 005 received from the server. Use parse_005 to get data out of this.
+## @Type API
+## @Note See http://www.irc.org/tech_docs/005.html for an incomplete list of 005 values.
+#---------------------------------------------------------------------
+server_005=""
+# NAMES output with UHNAMES and NAMESX
+# :photon.kuonet-ng.org 353 envbot = #bots :@%+AnMaster!AnMaster@staff.kuonet-ng.org @ChanServ!ChanServ@services.kuonet-ng.org bashbot!rfc3092@1F1794B2:769091B3
+# NAMES output with NAMESX only:
+# :hurricane.KuoNET.org 353 envbot = #test :bashbot ~@Brain ~@EmErgE &@AnMaster/kng
+#---------------------------------------------------------------------
+## 1 if UHNAMES enabled, otherwise 0
+## @Type API
+#---------------------------------------------------------------------
+server_UHNAMES=0
+#---------------------------------------------------------------------
+## 1 if NAMESX enabled, otherwise 0
+## @Type API
+#---------------------------------------------------------------------
+server_NAMESX=0
+# These are passed in a slightly odd way in 005 so we do them here.
+#---------------------------------------------------------------------
+## The mode char (if any) for ban excepts (normally +e)
+## @Type API
+#---------------------------------------------------------------------
+server_EXCEPTS=""
+#---------------------------------------------------------------------
+## The mode char (if any) for invite excepts (normally +I)
+## @Type API
+#---------------------------------------------------------------------
+server_INVEX=""
+
+# In case we don't get a 005, make some sane defaults.
+#---------------------------------------------------------------------
+## List channel modes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_CHMODES_LISTMODES="b"
+#---------------------------------------------------------------------
+## "Always parameters" channel modes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_CHMODES_ALWAYSPARAM="k"
+#---------------------------------------------------------------------
+## "Parameter on set" channel modes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_CHMODES_PARAMONSET="l"
+#---------------------------------------------------------------------
+## Simple channel modes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_CHMODES_SIMPLE="imnpst"
+#---------------------------------------------------------------------
+## Prefix channel modes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_PREFIX_modes="ov"
+#---------------------------------------------------------------------
+## Channel prefixes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_PREFIX_prefixes="@+"
+
+#---------------------------------------------------------------------
+## What is our current nick?
+## @Type API
+#---------------------------------------------------------------------
+server_nick_current=""
+#---------------------------------------------------------------------
+## 1 if we are connected, otherwise 0
+## @Type API
+#---------------------------------------------------------------------
+server_connected=0
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Get some common data out of 005, the whole will also be saved to
+## $server_005 for any module to use via parse_005().
+## This function is for cases that needs special action, like NAMESX
+## and UHNAMES.
+## This should be called directly after receiving a part of the 005!
+## @Type Private
+## @param The last part of the 005.
+#---------------------------------------------------------------------
+server_handle_005() {
+ # Example from freenode:
+ # :heinlein.freenode.net 005 envbot IRCD=dancer CAPAB CHANTYPES=# EXCEPTS INVEX CHANMODES=bdeIq,k,lfJD,cgijLmnPQrRstz CHANLIMIT=#:20 PREFIX=(ov)@+ MAXLIST=bdeI:50 MODES=4 STATUSMSG=@ KNOCK NICKLEN=16 :are supported by this server
+ # :heinlein.freenode.net 005 envbot SAFELIST CASEMAPPING=ascii CHANNELLEN=30 TOPICLEN=450 KICKLEN=450 KEYLEN=23 USERLEN=10 HOSTLEN=63 SILENCE=50 :are supported by this server
+ local line="$1"
+ if [[ $line =~ EXCEPTS(=([^ ]+))? ]]; then
+ # Some, but not all also send what char the modes for EXCEPTS is.
+ # If it isn't sent, lets guess it is +e
+ if [[ ${BASH_REMATCH[2]} ]]; then
+ server_EXCEPTS="${BASH_REMATCH[2]}"
+ else
+ server_EXCEPTS="e"
+ fi
+ fi
+ if [[ $line =~ INVEX(=([^ ]+))? ]]; then
+ # Some, but not all also send what char the modes for INVEX is.
+ # If it isn't sent, lets guess it is +I
+ if [[ ${BASH_REMATCH[2]} ]]; then
+ server_INVEX="${BASH_REMATCH[2]}"
+ else
+ server_INVEX="I"
+ fi
+ fi
+ if [[ $line =~ PREFIX=(\(([^ \)]+)\)([^ ]+)) ]]; then
+ server_PREFIX="${BASH_REMATCH[1]}"
+ server_PREFIX_modes="${BASH_REMATCH[2]}"
+ server_PREFIX_prefixes="${BASH_REMATCH[3]}"
+ fi
+ if [[ $line =~ CHANMODES=([^ ,]+),([^ ,]+),([^ ,]+),([^ ,]+) ]]; then
+ server_CHMODES_LISTMODES="${BASH_REMATCH[1]}"
+ server_CHMODES_ALWAYSPARAM="${BASH_REMATCH[2]}"
+ server_CHMODES_PARAMONSET="${BASH_REMATCH[3]}"
+ server_CHMODES_SIMPLE="${BASH_REMATCH[4]}"
+ fi
+ # Enable NAMESX is supported.
+ if [[ $line =~ NAMESX ]]; then
+ log_info "Enabled NAMESX support"
+ send_raw_flood "PROTOCTL NAMESX"
+ server_NAMESX=1
+ fi
+ # Enable UHNAMES if it is there.
+ if [[ $line =~ UHNAMES ]]; then
+ log_info "Enabled UHNAMES support"
+ send_raw_flood "PROTOCTL UHNAMES"
+ server_UHNAMES=1
+ fi
+}
+
+#---------------------------------------------------------------------
+## Respond to PING from server.
+## @Type Private
+## @param Raw line
+## @return 0 If it was a PING
+## @return 1 If it was not a PING
+#---------------------------------------------------------------------
+server_handle_ping() {
+ if [[ "$1" =~ ^PING\ *:(.*) ]] ;then
+ send_raw "PONG :${BASH_REMATCH[1]}"
+ return 0
+ fi
+ return 1
+}
+
+#---------------------------------------------------------------------
+## Handle numerics from server.
+## @Type Private
+## @param Numeric
+## @param Target (self)
+## @param Data
+#---------------------------------------------------------------------
+server_handle_numerics() {
+ # Slight sanity check
+ if [[ "$2" != "$server_nick_current" ]]; then
+ log_warning 'Own nick desynced!'
+ log_warning "It should be $server_nick_current but server says it is $2"
+ log_warning "Correcting own nick and lets hope that doesn't break anything"
+ server_nick_current="$2"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Handle NICK messages from server
+## @Type Private
+## @param Sender
+## @param New nick
+#---------------------------------------------------------------------
+server_handle_nick() {
+ local oldnick=
+ parse_hostmask_nick "$1" 'oldnick'
+ if [[ $oldnick == $server_nick_current ]]; then
+ server_nick_current="$2"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Handle nick in use.
+## @Type Private
+#---------------------------------------------------------------------
+server_handle_nick_in_use() {
+ if [[ $on_nick -eq 3 ]]; then
+ log_error "Third nick is ALSO in use. I give up"
+ bot_quit 2
+ elif [[ $on_nick -eq 2 ]]; then
+ log_warning "Second nick is ALSO in use, trying third"
+ send_nick "$config_thirdnick"
+ server_nick_current="$config_thirdnick"
+ on_nick=3
+ else
+ log_info_stdout "First nick is in use, trying second"
+ send_nick "$config_secondnick"
+ on_nick=2
+ # FIXME: THIS IS HACKISH AND MAY BREAK
+ server_nick_current="$config_secondnick"
+ fi
+ sleep 1
+}
+
+#---------------------------------------------------------------------
+## Connect to IRC server.
+## @Type Private
+#---------------------------------------------------------------------
+server_connect() {
+ server_connected=0
+ on_nick=1
+ # Clear current channels:
+ channels_current=""
+ # HACK: Clean up if we are aborted, replaced after connect with one that sends QUIT
+ trap 'transport_disconnect; rm -rvf "$tmp_home"; exit 1' TERM INT
+ log_info_stdout "Connecting to \"${config_server}:${config_server_port}\"..."
+ transport_connect "$config_server" "$config_server_port" "$config_server_ssl" "$config_server_bind" || return 1
+
+ [[ $config_server_passwd ]] && send_raw_flood_nolog "PASS $config_server_passwd"
+ log_info_stdout "logging in as $config_firstnick..."
+ send_nick "$config_firstnick"
+ # FIXME: THIS IS HACKISH AND MAY BREAK
+ server_nick_current="$config_firstnick"
+ # If a server password is set, send it.
+ send_raw_flood "USER $config_ident 0 * :${config_gecos}"
+ while true; do
+ line=
+ transport_read_line
+ local transport_status="$?"
+ # Still connected?
+ if ! transport_alive; then
+ return 1
+ fi
+ # Did we timeout waiting for data
+ # or did we get data?
+ if [[ $transport_status -ne 0 ]]; then
+ continue
+ fi
+ # Check with modules first, needed so we don't skip them.
+ for module in $modules_on_connect; do
+ module_${module}_on_connect "$line"
+ done
+ if [[ "$line" =~ ^:[^\ ]+\ +${numeric_RPL_MOTD} ]]; then
+ continue
+ fi
+ log_raw_in "$line"
+ if [[ "$line" =~ ^:[^\ ]+\ +([0-9]{3})\ +([^ ]+)\ +(.*) ]]; then
+ local numeric="${BASH_REMATCH[1]}"
+ # We use this to check below for our own nick.
+ local numericnick="${BASH_REMATCH[2]}"
+ local data="${BASH_REMATCH[3]}"
+ case "$numeric" in
+ "$numeric_RPL_MOTDSTART")
+ log_info "Motd is not displayed in log";
+ ;;
+ "$numeric_RPL_YOURHOST")
+ if [[ $line =~ ^:([^ ]+) ]]; then # just to get the server name, this should always be true
+ server_name="${BASH_REMATCH[1]}"
+ fi
+ ;;
+ "$numeric_RPL_WELCOME")
+ # This should work
+ server_nick_current="$numericnick"
+ ;;
+ # We don't care about these and don't want to show it as unhandled.
+ "$numeric_RPL_CREATED"|"$numeric_RPL_LUSERCLIENT"|"$numeric_RPL_LUSEROP"|"$numeric_RPL_LUSERUNKNOWN"|"$numeric_RPL_LUSERCHANNELS"|"$numeric_RPL_LUSERME"|"$numeric_RPL_LOCALUSERS"|"$numeric_RPL_GLOBALUSERS"|"$numeric_RPL_STATSCONN")
+ continue
+ ;;
+ "$numeric_RPL_MYINFO")
+ server_004="$data"
+ server_004=$(tr -d $'\r\n' <<< "$server_004") # Get rid of ending newline
+ ;;
+ "$numeric_RPL_ISUPPORT")
+ server_005+=" $data"
+ server_005=$(tr -d $'\r\n' <<< "$server_005") # Get rid of newlines
+ server_005="${server_005/ :are supported by this server/}" # Get rid of :are supported by this server
+ server_handle_005 "$line"
+ ;;
+ "$numeric_ERR_NICKNAMEINUSE"|"$numeric_ERR_ERRONEUSNICKNAME")
+ server_handle_nick_in_use
+ ;;
+ "$numeric_RPL_ENDOFMOTD"|"$numeric_ERR_NOMOTD")
+ sleep 1
+ log_info_stdout 'Connected'
+ server_connected=1
+ break
+ ;;
+ *)
+ if [[ -z "${numerics[10#${numeric}]}" ]]; then
+ log_info_file unknown_data.log "Unknown numeric during connect: $numeric Data: $data"
+ else
+ log_info_file unknown_data.log "Known but not handled numeric during connect: $numeric Data: $data"
+ fi
+ ;;
+ esac
+ fi
+ server_handle_ping "$line"
+ done
+}
diff --git a/lib/time.sh b/lib/time.sh
new file mode 100644
index 0000000..befce82
--- /dev/null
+++ b/lib/time.sh
@@ -0,0 +1,110 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Functions for working with time.
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Check if a set time has passed
+## @Type API
+## @param Unix timestamp to check against
+## @param Number of seconds
+## @return 0 If at least the given number of seconds has passed
+## @return 1 If it hasn't
+#---------------------------------------------------------------------
+time_check_interval() {
+ local newtime=
+ time_get_current 'newtime'
+ (( ( newtime - $1 ) > $2 ))
+}
+
+
+#---------------------------------------------------------------------
+## Get current time (seconds since 1970-01-01 00:00:00 UTC)
+## @Type API
+## @param Variable to return current timestamp in
+#---------------------------------------------------------------------
+time_get_current() {
+ printf -v "$1" '%s' "$(( time_initial + SECONDS ))"
+}
+
+
+#---------------------------------------------------------------------
+## Returns how long a time interval is in a human readable format.
+## @Type API
+## @param Time interval
+## @param Variable to return result in.
+## @Note Modified version of function posted by goedel at
+## @Note http://forum.bash-hackers.org/index.php?topic=59.0
+#---------------------------------------------------------------------
+time_format_difference() {
+ local tdiv=$1
+ local tmod i
+ local output=""
+
+ for ((i=0; i < ${#time_format_units[@]}; ++i)); do
+ # n means no limit.
+ if [[ ${time_format_unitspan[i]} == n ]]; then
+ tmod=$tdiv
+ else
+ (( tmod = tdiv % time_format_unitspan[i] ))
+ (( tdiv = tdiv / time_format_unitspan[i] ))
+ fi
+ output="$tmod${time_format_units[i]} $output"
+ [[ $tdiv = 0 ]] && break
+ done
+
+ printf -v "$2" '%s' "${output% }"
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Array used for time_format_difference
+## @Type Private
+#---------------------------------------------------------------------
+declare -r time_format_units=( s min h d mon )
+#---------------------------------------------------------------------
+## Array used for time_format_difference
+## @Type Private
+## @Note n means no limit.
+#---------------------------------------------------------------------
+declare -r time_format_unitspan=( 60 60 24 30 n )
+
+#---------------------------------------------------------------------
+## Initial timestamp that we use to get current time later on.
+## @Type Private
+#---------------------------------------------------------------------
+time_initial=''
+
+#---------------------------------------------------------------------
+## Set up time variables
+## @Type Private
+#---------------------------------------------------------------------
+time_init() {
+ # Set up initial env
+ time_initial="$(date -u +%s)"
+ SECONDS=0
+}