diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/access.sh | 99 | ||||
-rw-r--r-- | lib/channels.sh | 125 | ||||
-rw-r--r-- | lib/commands.sh | 265 | ||||
-rw-r--r-- | lib/config.sh | 219 | ||||
-rw-r--r-- | lib/debug.sh | 84 | ||||
-rw-r--r-- | lib/feedback.sh | 59 | ||||
-rw-r--r-- | lib/hash.sh | 312 | ||||
-rw-r--r-- | lib/log.sh | 285 | ||||
-rw-r--r-- | lib/main.sh | 566 | ||||
-rw-r--r-- | lib/misc.sh | 267 | ||||
-rw-r--r-- | lib/modules.sh | 447 | ||||
-rw-r--r-- | lib/numerics.sh | 348 | ||||
-rw-r--r-- | lib/parse.sh | 100 | ||||
-rw-r--r-- | lib/send.sh | 178 | ||||
-rw-r--r-- | lib/server.sh | 337 | ||||
-rw-r--r-- | lib/time.sh | 110 |
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 +} |