353 lines
13 KiB
Bash
Executable File
353 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
if [ "$(whoami)" = "root" ]; then
|
|
echo "This bot should not be ran as root."
|
|
exit 1
|
|
fi
|
|
|
|
# Check if bot.cfg exists, if not create it from example
|
|
if [[ ! -f "bot.cfg" ]]; then
|
|
if [[ -f "bot.cfg.example" ]]; then
|
|
echo "bot.cfg not found. Creating from bot.cfg.example..."
|
|
cp "bot.cfg.example" "bot.cfg"
|
|
echo "Please edit bot.cfg to configure your bot (server, channel, nick, etc.)."
|
|
exit 0
|
|
else
|
|
echo "Neither bot.cfg nor bot.cfg.example found."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Check and initialize customizable configuration files
|
|
check_config_files() {
|
|
local configFiles=(
|
|
"response/error.txt:response/error.txt.example"
|
|
"response/exit.txt:response/exit.txt.example"
|
|
"triggers/greet/greetings.txt:triggers/greet/greetings.txt.example"
|
|
"triggers/keywords/keywords.cfg:triggers/keywords/keywords.cfg.example"
|
|
)
|
|
|
|
for entry in "${configFiles[@]}"; do
|
|
local target="${entry%:*}"
|
|
local example="${entry#*:}"
|
|
|
|
if [[ ! -f "$target" ]]; then
|
|
if [[ -f "$example" ]]; then
|
|
cp "$example" "$target"
|
|
echo "Created $target from $example"
|
|
else
|
|
# Hardcoded fallback for critical files if example is missing
|
|
case "$target" in
|
|
"response/error.txt")
|
|
mkdir -p "$(dirname "$target")"
|
|
printf "I don't understand\nI'm not sure how to help you with that.\n" > "$target"
|
|
echo "Warning: $example not found. Created $target with default content."
|
|
;;
|
|
"response/exit.txt")
|
|
mkdir -p "$(dirname "$target")"
|
|
printf "Ta ta for now.\nbye.\n" > "$target"
|
|
echo "Warning: $example not found. Created $target with default content."
|
|
;;
|
|
*)
|
|
echo "Warning: Missing $example and no hardcoded fallback available."
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
done
|
|
}
|
|
|
|
check_config_files
|
|
|
|
# Load required files.
|
|
for i in "bot.cfg" "functions.sh" ; do
|
|
if [[ -f "$i" ]]; then
|
|
source "$i"
|
|
else
|
|
echo "Could not find required file \"${i}\"."
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Variables important to modules need to be exported here.
|
|
export allowList
|
|
export channels
|
|
export input
|
|
export ignoreList
|
|
export nick
|
|
export quitMessage
|
|
export intentionalExit
|
|
|
|
# Check for critical dependencies needed by the bot core
|
|
coreDependencies=("socat" "tail" "shuf" "grep" "sed" "tr" "cut" "date")
|
|
missingCore=()
|
|
for dep in "${coreDependencies[@]}"; do
|
|
if ! command -v "$dep" &> /dev/null; then
|
|
missingCore+=("$dep")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#missingCore[@]} -gt 0 ]]; then
|
|
echo "ERROR: Missing critical dependencies: ${missingCore[*]}"
|
|
echo "Please install these packages before running the bot."
|
|
exit 1
|
|
fi
|
|
|
|
# Clean up any leftover temporary files from previous runs
|
|
cleanup_old_tempfiles() {
|
|
find . -maxdepth 1 -type f -name '.[A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9]' -mmin +5 -delete 2>/dev/null
|
|
}
|
|
|
|
# Function called on exit to remove the temporary input file.
|
|
rm_input() {
|
|
if [[ -f "$input" ]]; then
|
|
rm -f "$input"
|
|
fi
|
|
}
|
|
|
|
# Function to trim log file to prevent unbounded growth
|
|
# Keeps log between 800-1000 lines by trimming oldest entries when limit reached
|
|
trim_log() {
|
|
local logFile="$1"
|
|
local maxLines=1000
|
|
local trimTo=800
|
|
|
|
# Only check if log file exists and is readable
|
|
if [[ ! -f "$logFile" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Count lines efficiently
|
|
local lineCount
|
|
lineCount=$(wc -l < "$logFile" 2>/dev/null || echo 0)
|
|
|
|
# If over limit, trim to keep most recent entries
|
|
if [[ $lineCount -gt $maxLines ]]; then
|
|
tail -n "$trimTo" "$logFile" > "${logFile}.tmp" && mv "${logFile}.tmp" "$logFile"
|
|
fi
|
|
}
|
|
|
|
# Trap exiting ffrom the program to remove the temporary input file.
|
|
trap rm_input EXIT
|
|
|
|
# Flag to track intentional shutdown (set by exit module)
|
|
intentionalExit=false
|
|
|
|
# Reconnection loop - keeps bot connected even if connection drops
|
|
reconnectDelay=10
|
|
while true; do
|
|
# Clean up old temp files from previous runs
|
|
cleanup_old_tempfiles
|
|
|
|
# Set up the connection.
|
|
echo -e "Session started $(date "+%I:%M%p%n %A, %B %d, %Y").\n\nTo gracefully exit, make sure you are in the allow list and send the command exit to the bot.\n\n" | tee -a "$log"
|
|
echo "NICK $nick" > "$input"
|
|
echo "USER $user" >> "$input"
|
|
# Join all configured channels
|
|
for channelName in "${channels[@]}"; do
|
|
echo "JOIN #$channelName" >> "$input"
|
|
done
|
|
|
|
# The main loop of the program where we watch for output from irc.
|
|
# Use SSL if enabled, otherwise plain TCP
|
|
if [[ "${useSSL,,}" == "true" ]]; then
|
|
socatAddress="SSL:${server}:${port},verify=0"
|
|
else
|
|
socatAddress="TCP:${server}:${port}"
|
|
fi
|
|
|
|
# Counter for log trimming (check every 50 messages)
|
|
logTrimCounter=0
|
|
|
|
tail -f "$input" | socat -,ignoreeof "$socatAddress" | while read -r result ; do
|
|
# Strip carriage return from IRC protocol (CRLF line endings)
|
|
result="${result%$'\r'}"
|
|
# Sanitize control characters for logging (prevent log injection)
|
|
logSanitized="${result//[$'\001'-$'\037'$'\177']/}"
|
|
# log the session
|
|
echo "$logSanitized [$(date "+$dateFormat")]" >> "$log"
|
|
|
|
# Periodically trim log to prevent unbounded growth
|
|
((logTrimCounter++))
|
|
if [[ $logTrimCounter -ge 50 ]]; then
|
|
trim_log "$log"
|
|
logTrimCounter=0
|
|
fi
|
|
|
|
# do things when you see output
|
|
case "$result" in
|
|
# Handle nick changes
|
|
":"*"NICK :"*)
|
|
# Get the original nick
|
|
originalNick="${result#:}"
|
|
originalNick="${originalNick%%!*}"
|
|
# If the old nick was in the ignore list, update it.
|
|
if [[ "${originalNick}" =~ ^($ignoreList)$ ]]; then
|
|
export ignoreList="${ignoreList/${originalNick}/${result#:*:}}"
|
|
fi
|
|
;;
|
|
# respond to ping requests from the server
|
|
PING*)
|
|
echo "${result/I/O}" >> "$input"
|
|
;;
|
|
# for pings on nick/user
|
|
*"You have not"*)
|
|
for channelName in "${channels[@]}"; do
|
|
echo "JOIN #$channelName" | tee -a "$input"
|
|
done
|
|
;;
|
|
# Run on kick
|
|
:*!*@*" KICK "*" $nick :"*)
|
|
if [ "$autoRejoinChannel" = "true" ]; then
|
|
# Extract channel name from kick message and rejoin that specific channel
|
|
kickedChannel="${result##*#}"
|
|
kickedChannel="#${kickedChannel%% *}"
|
|
echo "JOIN $kickedChannel" | tee -a "$input"
|
|
fi
|
|
if [ "$curseKicker" = "true" ]; then
|
|
kickerName="${result%!*}"
|
|
kickerName="${kickerName:1}"
|
|
kickerChannel="${result##*#}"
|
|
kickerChannel="#${kickerChannel%% *}"
|
|
msg "$kickerChannel" "$kickerName: $(shuf -e -n1 "fuck you" "go fuck yourself")!"
|
|
fi
|
|
;;
|
|
# run when someone joins
|
|
*"JOIN :#"*)
|
|
who="${result%%!*}"
|
|
who="${who:1}"
|
|
from="${result#*#}"
|
|
from="#$from"
|
|
if [ "$who" = "$nick" ]; then
|
|
continue
|
|
fi
|
|
if [ "${greet^^}" = "TRUE" ]; then
|
|
set -f
|
|
./triggers/greet/greet.sh "$who" "$from"
|
|
set +f
|
|
fi
|
|
;;
|
|
# run when someone leaves
|
|
*"PART #"*)
|
|
who="${result%%!*}"
|
|
who="${who:1}"
|
|
from="${result#*#}"
|
|
from="#$from"
|
|
if [ "$who" = "$nick" ]; then
|
|
continue
|
|
fi
|
|
if [ "${leave^^}" = "TRUE" ]; then
|
|
set -f
|
|
./triggers/bye/bye.sh "$who" "$from"
|
|
set +f
|
|
fi
|
|
;;
|
|
# run when a private message is seen
|
|
*"PRIVMSG "[[:alnum:]-_]*)
|
|
echo "$logSanitized" >> "$log"
|
|
who="${result%%!*}"
|
|
who="${who:1}"
|
|
from="${who%!*}"
|
|
command="${result#:* PRIVMSG [[:alnum:]_-]*:}"
|
|
command="${command//# /}"
|
|
will="${command#* }"
|
|
command="${command%% *}"
|
|
if [[ "$from" =~ ^($allowList)$ ]]; then
|
|
if command -v "./modules/${command% *}/${command% *}.sh" ; then
|
|
willSanitized="${will//[$'\001'-$'\037'$'\177']/}"
|
|
echo "Calling module ./modules/${command% *}/${command% *}/${command% *}.sh \"$who\" \"$from\" $willSanitized" >> "$log"
|
|
# Disable wildcards
|
|
set -f
|
|
# For PMs, respond directly to the user, not to a channel
|
|
"./modules/${command% *}/${command% *}.sh" "$who" "$from" "$will"
|
|
# Enable wildcards
|
|
set +f
|
|
else
|
|
reply "$who" "$(shuf -n1 "response/error.txt")"
|
|
fi
|
|
else
|
|
reply "$who" "You are not in the allowed list for this bot. If you think this is an error, please contact the bot's administrator."
|
|
fi
|
|
;;
|
|
# run when a message is seen
|
|
*PRIVMSG*)
|
|
# Only process if this is a user message (contains ! for hostmask)
|
|
if [[ ! "$result" =~ :[^!]+!.*PRIVMSG ]]; then
|
|
continue
|
|
fi
|
|
|
|
who="${result%%!*}"
|
|
who="${who:1}"
|
|
from="${result#*#}"
|
|
from="${from%% *}"
|
|
from="#${from:-${channels[0]}}"
|
|
|
|
# Validate channel name format (IRC RFC 2812: must start with #, contain only valid chars)
|
|
if [[ ! "$from" =~ ^#[a-zA-Z0-9_-]+$ ]]; then
|
|
# Invalid channel, use default
|
|
from="#${channels[0]}"
|
|
fi
|
|
|
|
# Trigger stuff happens here.
|
|
# Call link trigger if msg contains a link:
|
|
if [[ "$result" =~ .*http://|https://|www\..* ]]; then
|
|
set -f
|
|
echo "Calling link.sh with \"$who\" \"$from\" \"$logSanitized\"" >> "$log"
|
|
./triggers/link/link.sh "$who" "$from" "$result"
|
|
set -f
|
|
# Although this calls modules, it triggers on text other than the bot's nick
|
|
# To make sure that modules are only called when they are supposed to be, had to combine string monipulation with regexp.
|
|
elif [[ "${result#:*PRIVMSG*:}" =~ ^[${botCaller}][a-zA-Z0-9_].* ]]; then
|
|
echo "DEBUG: Matched bot caller pattern" >> "$log"
|
|
command="${result#*:[[:punct:]]}"
|
|
command="${command//# /}"
|
|
will="${command#* }"
|
|
command="${command%% *}"
|
|
# If will equals command, there were no arguments
|
|
[[ "$will" == "$command" ]] && will=""
|
|
willSanitized="${will//[$'\001'-$'\037'$'\177']/}"
|
|
echo "DEBUG: command='$command' will='$willSanitized'" >> "$log"
|
|
if command -v "./modules/${command% *}/${command% *}.sh" &>/dev/null ; then
|
|
echo "Calling module ./modules/${command% *}/${command% *}.sh \"$who\" \"$from\" $willSanitized" >> "$log"
|
|
# Disable wildcards
|
|
set -f
|
|
"./modules/${command% *}/${command% *}.sh" "$who" "$from" "$will"
|
|
# Enable wildcards
|
|
set +f
|
|
else
|
|
./modules/say/say.sh "$who" "$from" "$who: $(shuf -n1 "response/error.txt")"
|
|
fi
|
|
else
|
|
if ! [[ "$who" =~ ^($ignoreList)$ ]]; then
|
|
set -f
|
|
./triggers/keywords/keywords.sh "$who" "$from" "$result"
|
|
# Only call wordtrack for valid channel messages
|
|
if [[ "$from" =~ ^#[a-zA-Z0-9_-]+$ ]]; then
|
|
# Extract just the message text for wordtrack
|
|
messageText="${result#*PRIVMSG*:}"
|
|
./triggers/wordtrack/wordtrack.sh "$who" "$from" "$messageText"
|
|
fi
|
|
set +f
|
|
fi
|
|
fi
|
|
;;
|
|
*)
|
|
echo "$logSanitized" >> "$log"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# If we reach here, the connection was dropped
|
|
# Check if this was an intentional exit
|
|
if [[ "$intentionalExit" == "true" ]]; then
|
|
echo "Bot shutdown requested. Exiting. [$(date "+$dateFormat")]" | tee -a "$log"
|
|
break
|
|
fi
|
|
|
|
echo "Connection lost. Reconnecting in $reconnectDelay seconds... [$(date "+$dateFormat")]" | tee -a "$log"
|
|
sleep "$reconnectDelay"
|
|
done
|
|
|
|
rm_input
|
|
exit 0
|