#!/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 . #
# #
###########################################################################
#---------------------------------------------------------------------
## 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"
}
# Check for moreutils else we're doomed.
if ! which sponge
then
echo "moreutils is a dep, please install."
envbot_quit 1
fi
# Check for w3m because we use it to convert html entities.
if ! which w3m
then
echo "w3m is a dep, please install."
envbot_quit 1
fi
# Check for recode
if ! which recode
then
echo "recode is a dep, please install."
envbot_quit 1
fi
# 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.
## -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"
echo no > pbot_present
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"
# What happens next is important
config_update_time=-100
time_n0w=$( date +%s )
# If it's been more than a minute since we updated the config.
if (( ( time_n0w - 60 ) > config_update_time ))
then
source process_event
config_update_time=${time_n0w}
fi
process_event
# 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
[[ ${sender%%!*} == pbot ]] && echo yes > pbot_present
my_own_name='pbot'
person="${sender%%!*}"
# Remove any forward slashes.
personoslash="${person//\/}"
declare -l personoslashlower="${personoslash}"
the_time_now=$(date +%s)
# If someone has sent this person a message then tell them.
if [[ -f "announcements/people/${personoslashlower}/messages" ]]
then
yepyep=1
# Make sure they have not already been
# told they have a message less than 1
# hour ago
if [[ -f "announcements/people/${personoslashlower}/seen" ]] && (( ( $( stat -c %Y "announcements/people/${personoslashlower}/you_have_mail" ) + 3600 ) > the_time_now ))
then
yepyep=0
fi
if (( yepyep ))
then
if (( $(wc -l "announcements/people/${personoslashlower}/messages" | cut -d ' ' -f 1) > 1 ))
then
send_msg "${channel}" "${personoslash}: you have messages, type something to see them."
else
send_msg "${channel}" "${personoslash}: you have a message, type something to see it."
fi
touch "announcements/people/${personoslashlower}/you_have_mail"
fi
fi
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
[[ ${sender%%!*} == pbot ]] && echo no > pbot_present
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"