448 lines
16 KiB
Bash
448 lines
16 KiB
Bash
#!/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
|
|
}
|