Reorganization to hopefully prevent git conflicts.

This commit is contained in:
Storm Dragon
2025-10-25 01:30:02 -04:00
parent f6990bcc81
commit d684623974
13 changed files with 296 additions and 77 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,7 @@
response/error.txt response/error.txt
response/exit.txt
triggers/greet/greetings.txt
triggers/keywords/keywords.cfg
log.txt log.txt
.* .*
bot.cfg bot.cfg

48
bot.sh
View File

@@ -18,6 +18,47 @@ if [[ ! -f "bot.cfg" ]]; then
fi fi
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. # Load required files.
for i in "bot.cfg" "functions.sh" ; do for i in "bot.cfg" "functions.sh" ; do
if [[ -f "$i" ]]; then if [[ -f "$i" ]]; then
@@ -231,6 +272,13 @@ while true; do
from="${result#*#}" from="${result#*#}"
from="${from%% *}" from="${from%% *}"
from="#${from:-${channels[0]}}" 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. # Trigger stuff happens here.
# Call link trigger if msg contains a link: # Call link trigger if msg contains a link:
if [[ "$result" =~ .*http://|https://|www\..* ]]; then if [[ "$result" =~ .*http://|https://|www\..* ]]; then

View File

@@ -14,4 +14,11 @@ if ! check_dependencies "${dependencies[@]}"; then
exit 1 exit 1
fi fi
msg "$chan" "$(units -v ${*#* } | head -n1 | tr -d '[:space:]')" # Validate input
if [[ -z "$*" ]]; then
msg "$chan" "Please provide a unit conversion (e.g., '10 meters to feet')."
exit 0
fi
# Quote variables to prevent command injection
msg "$chan" "$(units -v "${*#* }" | head -n1 | tr -d '[:space:]')"

View File

@@ -5,6 +5,9 @@
dependencies=("fortune") dependencies=("fortune")
target="${3#fortune}" target="${3#fortune}"
# Trim leading/trailing whitespace
target="${target#"${target%%[![:space:]]*}"}"
target="${target%"${target##*[![:space:]]}"}"
# Check dependencies before running # Check dependencies before running
if ! check_dependencies "${dependencies[@]}"; then if ! check_dependencies "${dependencies[@]}"; then
@@ -12,6 +15,7 @@ if ! check_dependencies "${dependencies[@]}"; then
exit 1 exit 1
fi fi
fortuneText="$(fortune -a -e -s -n 512 $target || echo "No fortunes found.")" # Quote target to prevent command injection
fortuneText="$(fortune -a -e -s -n 512 "$target" || echo "No fortunes found.")"
fortuneText="$(echo "$fortuneText" | tr '[:space:]' ' ' | sed -e 's/"/\"/g')" fortuneText="$(echo "$fortuneText" | tr '[:space:]' ' ' | sed -e 's/"/\"/g')"
msg "$2" "$fortuneText" msg "$2" "$fortuneText"

View File

@@ -4,8 +4,35 @@
user=$1 user=$1
shift shift
shift shift
if [[ "$user" =~ $allowList ]]; then
./modules/do/do.sh "$1" "#$channel" "does a magical gesture and turns into ${1}!" newNick="$1"
nick $1
sed -i bot.cfg -e "s/nick=.*/nick=\"$1\"/" # Validate that user is authorized
if [[ ! "$user" =~ $allowList ]]; then
exit 0
fi fi
# Validate IRC nickname format (RFC 2812)
# Nicknames can contain: a-z A-Z 0-9 _ - [ ] { } \ | ^
if [[ -z "$newNick" ]]; then
msg "#$channel" "$user: Please provide a nickname."
exit 0
fi
if ! [[ "$newNick" =~ ^[a-zA-Z0-9_\[\]\{\}\\|\^-]+$ ]]; then
msg "#$channel" "$user: Invalid nickname format. Only alphanumeric and _-[]{}\\|^ allowed."
exit 0
fi
if [[ ${#newNick} -gt 30 ]]; then
msg "#$channel" "$user: Nickname too long (max 30 characters)."
exit 0
fi
# Change the nick
./modules/do/do.sh "$newNick" "#$channel" "does a magical gesture and turns into ${newNick}!"
nick "$newNick"
# Safely update config file - escape forward slashes for sed
escapedNick="${newNick//\//\\/}"
sed -i "s/^nick=.*/nick=\"${escapedNick}\"/" bot.cfg

View File

@@ -13,15 +13,15 @@ if ! check_dependencies "${dependencies[@]}"; then
msg "$chan" "$1: This module requires: ${dependencies[*]}" msg "$chan" "$1: This module requires: ${dependencies[*]}"
exit 1 exit 1
fi fi
#get the lyric text into a variable # Get the lyric text into a variable (quote $@ to prevent command injection)
lyricText="$(clyrics $@ | tr '[:space:]' ' ' | tr -s ' ' | fold -s -w 384)" lyricText="$(clyrics "$@" | tr '[:space:]' ' ' | tr -s ' ' | fold -s -w 384)"
i=$(echo "$lyricText" | wc -l) i=$(echo "$lyricText" | wc -l)
i=$(($RANDOM % $i + 1)) i=$((RANDOM % i + 1))
lyricText="$(echo "$lyricText" | tail +$i | head -1 | rev | cut -d '.' -f2- | rev)" lyricText="$(echo "$lyricText" | tail +$i | head -1 | rev | cut -d '.' -f2- | rev)"
#Display the lyric text #Display the lyric text
if [ ${#lyricText} -gt 15 ] ; then if [ ${#lyricText} -gt 15 ] ; then
msg "$chan" "${lyricText}" msg "$chan" "${lyricText}"
exit 0 exit 0
fi fi
msg "$chan" "no lyrics found for $@." msg "$chan" "no lyrics found for $*."
exit 0 exit 0

View File

@@ -1,16 +1,26 @@
#!/usr/bin/env bash #!/usr/bin/env bash
[ -f functions.sh ] && source functions.sh [ -f functions.sh ] && source functions.sh
greetingsFile="triggers/greet/greetings.txt"
# All names to match are completely lowercase. # All names to match are completely lowercase.
case "${1,,}" in case "${1,,}" in
storm_dragon) storm_dragon)
msg "$2" "my lord, $1: how may I serve you?" msg "$2" "my lord, $1: how may I serve you?"
;; ;;
*) *)
greeting=( # Read greetings from file into array
Greetings if [[ -f "$greetingsFile" ]]; then
"Howdy, welcome to $2!" mapfile -t greeting < "$greetingsFile"
"Wazzup Moe Fugger!" else
"Welcome to $2!" # Fallback if file doesn't exist
) greeting=("Greetings" "Welcome!")
msg "$2" "$1: ${greeting[$(($RANDOM % ${#greeting[@]}))]}" fi
# Replace {channel} placeholder with actual channel name
selectedGreeting="${greeting[$((RANDOM % ${#greeting[@]}))]}"
selectedGreeting="${selectedGreeting//\{channel\}/$2}"
msg "$2" "$1: $selectedGreeting"
;;
esac esac

View File

@@ -0,0 +1,4 @@
Greetings
Howdy, welcome to {channel}!
Wazzup Moe Fugger!
Welcome to {channel}!

View File

@@ -0,0 +1,26 @@
# Keywords Configuration
# Format: keyword|action|percentage (percentage is optional, defaults to 100%)
# Available variables: $chan (channel), $who (user nickname)
# Use {random:option1|option2|option3} for random selection in messages
#
# Examples:
# word|msg "$chan" "Hello there!"|50%
# test|act "$chan" "does something cool"
linux|msg "$chan" "Linux is {random:awesome|God|great|lovely|fantastic|amazing|wonderful}!"|25%
windows|msg "$chan" "{random:Failure is not an option, it comes bundled with Windows!|Apple got all pissed off because I farted in their store. It's not my falt they don't have Windows...|Windows is dumb!|Did you know that Micro Soft is Linda's pet name for Bill Gates?|A computer without Windows is like a chocolate cake without the mustard.|Windows is stupid|In a world without walls and fences - who needs windows and gates?|Windows, plug and pray.|Windows - Just another pain in the glass.|Windows, it's not pretty, it's not ugly, but it's pretty ugly.}!"|25%
emacs|msg "$chan" "$who, Real men of genius use vim!"|50%
jaws|msg "$chan" "${who}: watch out for sharks!"
emacspeak|msg "$chan" "$who, Real men of genius use vim!"
nano|msg "$chan" "$who, Real men of genius use vim!"
pidgin|msg "$chan" "$who, Real men of genius use irssi!"
weechat|msg "$chan" "$who, Real men of genius use irssi!"
thunderbird|msg "$chan" "$who, Real dogs use mutt, real men of genius use cat on a mailbox file!"
gedit|msg "$chan" "$who, Real men of genius use vim!"
pluma|msg "$chan" "$who, Real men of genius use vim!"
dragonforce|msg "$chan" "$who: I love DragonForce!!!"
vim|msg "$chan" "{random:Praise vim! HA|In times of trouble, just ask yourself, 'What would Bram Moolenaar do?'.|Vim is like a Ferrari, if you're a beginner, it handles like a bitch, but once you get the hang of it, it's small, powerful and FAST!|VIM is like a new model Ferrari, and sounds like one too - 'VIIIIIIMMM!'|Only through vim can you be saved! HA}"
# Multi-word triggers (match anywhere in message, spaces removed)
# Format: ~phrase|action|percentage
~nowplaying:|act "$chan" "{random:cranks the volume up to 11|got soooo high at that show|boogies down to the sound of the band}!"

View File

@@ -4,67 +4,142 @@
who="${1%!*}" who="${1%!*}"
who="${who//:}" who="${who//:}"
shift shift
# shellcheck disable=SC2034 # Used in action strings via execute_action
chan="$1" chan="$1"
shift shift
# each word is stored in an associative array, with the actions to be taken as the array's contents. keywordsFile="triggers/keywords/keywords.cfg"
# the variable $chan contains the channel that caused the trigger.
# the variable $who contains the nick that caused the trigger.
# Optional: Add a percentage (e.g., "50%") as the last element to respond only that percent of the time.
declare -A keywords
keywords[linux]="msg \"$chan\" \"Linux is $(shuf -n1 -e awesome God great lovely fantastic amazing wonderful)!\" 25%"
keywords[windows]="msg \"$chan\" \"$(shuf -n1 -e\
"Failure is not an option, it comes bundled with Windows!"\
"Apple got all pissed off because I farted in their store. It's not my falt they don't have Windows..."\
"Windows is dumb!"\
"Did you know that Micro Soft is Linda's pet name for Bill Gates?"\
"A computer without Windows is like a chocolate cake without the mustard."\
"Windows is stupid"\
"In a world without walls and fences - who needs windows and gates?"\
"Windows, plug and pray."\
"Windows - Just another pain in the glass."\
"Windows, it's not pretty, it's not ugly, but it's pretty ugly.")!\" 25%"
keywords[emacs]="msg \"$chan\" \"$who, Real men of genius use vim!\" 50%"
keywords[jaws]="msg \"$chan\" \"${who}: watch out for sharks!\""
keywords[emacspeak]="msg \"$chan\" \"$who, Real men of genius use vim!\""
keywords[nano]="msg \"$chan\" \"$who, Real men of genius use vim!\""
keywords[pidgin]="msg \"$chan\" \"$who, Real men of genius use irssi!\""
keywords[weechat]="msg \"$chan\" \"$who, Real men of genius use irssi!\""
keywords[thunderbird]="msg \"$chan\" \"$who, Real dogs use mutt, real men of genius use cat on a mailbox file!\""
keywords[gedit]="msg \"$chan\" \"$who, Real men of genius use vim!\""
keywords[pluma]="msg \"$chan\" \"$who, Real men of genius use vim!\""
keywords[dragonforce]="msg \"$chan\" \"$who: I love DragonForce!!!\""
keywords[vim]="msg \"$chan\" \"$(shuf -n1 -e \
"Praise vim! HA"\
"In times of trouble, just ask yourself, 'What would Bram Moolenaar do?'."\
"Vim is like a Ferrari, if you're a beginner, it handles like a bitch, but once you get the hang of it, it's small, powerful and FAST!"\
"VIM is like a new model Ferrari, and sounds like one too - 'VIIIIIIMMM!'"\
"Only through vim can you be saved! HA")\""
# Function to process random selection syntax: {random:opt1|opt2|opt3}
process_random() {
local text="$1"
while [[ "$text" =~ \{random:([^}]+)\} ]]; do
local options="${BASH_REMATCH[1]}"
IFS='|' read -ra optArray <<< "$options"
local selected="${optArray[$((RANDOM % ${#optArray[@]}))]}"
text="${text/\{random:$options\}/$selected}"
done
echo "$text"
}
# Safe execution function - only allows predefined IRC functions
execute_action() {
local action="$1"
# Parse the action to extract function name and arguments
if [[ "$action" =~ ^msg[[:space:]]+(\"[^\"]+\"|[^[:space:]]+)[[:space:]]+(.+)$ ]]; then
local target="${BASH_REMATCH[1]}"
local message="${BASH_REMATCH[2]}"
# Remove quotes from target and message if present
target="${target//\"/}"
# Safely expand only $chan and $who variables - NO EVAL
message="${message//\"\$chan\"/$chan}"
message="${message//\$chan/$chan}"
message="${message//\"\$who\"/$who}"
message="${message//\$who/$who}"
message="${message//\$\{chan\}/$chan}"
message="${message//\$\{who\}/$who}"
message="$(process_random "$message")"
msg "$target" "$message"
elif [[ "$action" =~ ^act[[:space:]]+(\"[^\"]+\"|[^[:space:]]+)[[:space:]]+(.+)$ ]]; then
local target="${BASH_REMATCH[1]}"
local message="${BASH_REMATCH[2]}"
# Remove quotes from target and message if present
target="${target//\"/}"
# Safely expand only $chan and $who variables - NO EVAL
message="${message//\"\$chan\"/$chan}"
message="${message//\$chan/$chan}"
message="${message//\"\$who\"/$who}"
message="${message//\$who/$who}"
message="${message//\$\{chan\}/$chan}"
message="${message//\$\{who\}/$who}"
message="$(process_random "$message")"
act "$target" "$message"
elif [[ "$action" =~ ^reply[[:space:]]+(\"[^\"]+\"|[^[:space:]]+)[[:space:]]+(.+)$ ]]; then
local target="${BASH_REMATCH[1]}"
local message="${BASH_REMATCH[2]}"
# Remove quotes from target and message if present
target="${target//\"/}"
# Safely expand only $chan and $who variables - NO EVAL
message="${message//\"\$chan\"/$chan}"
message="${message//\$chan/$chan}"
message="${message//\"\$who\"/$who}"
message="${message//\$who/$who}"
message="${message//\$\{chan\}/$chan}"
message="${message//\$\{who\}/$who}"
message="$(process_random "$message")"
reply "$target" "$message"
fi
}
# Load keywords from config file into associative array
declare -A keywords
if [[ -f "$keywordsFile" ]]; then
while IFS='|' read -r keyword action percentage || [[ -n "$keyword" ]]; do
# Skip comments and empty lines
[[ "$keyword" =~ ^[[:space:]]*# ]] && continue
[[ -z "$keyword" ]] && continue
# Trim whitespace
keyword="${keyword#"${keyword%%[![:space:]]*}"}"
keyword="${keyword%"${keyword##*[![:space:]]}"}"
action="${action#"${action%%[![:space:]]*}"}"
action="${action%"${action##*[![:space:]]}"}"
percentage="${percentage#"${percentage%%[![:space:]]*}"}"
percentage="${percentage%"${percentage##*[![:space:]]}"}"
# Store in array (key includes ~ prefix for multi-word triggers)
if [[ -n "$percentage" ]]; then
keywords["$keyword"]="$action $percentage"
else
keywords["$keyword"]="$action"
fi
done < "$keywordsFile"
fi
# Process single-word triggers
wordList="$(echo "${@,,}" | tr '[:space:]' $'\n' | sort -u)" wordList="$(echo "${@,,}" | tr '[:space:]' $'\n' | sort -u)"
for w in ${wordList//[[:punct:]]/} ; do for w in ${wordList//[[:punct:]]/} ; do
if [[ -n "${keywords[${w,,}]}" && "$lastWordMatch" != "${keywords[${w,,}]}" ]]; then if [[ -n "${keywords[${w,,}]}" && "$lastWordMatch" != "${keywords[${w,,}]}" ]]; then
keywordAction="${keywords[${w,,}]}" keywordAction="${keywords[${w,,}]}"
# Check if the last element is a percentage # Check if the last element is a percentage
if [[ "$keywordAction" =~ (.*)\ ([0-9]+)%$ ]]; then if [[ "$keywordAction" =~ (.*)\ ([0-9]+)%$ ]]; then
command="${BASH_REMATCH[1]}" command="${BASH_REMATCH[1]}"
percentage="${BASH_REMATCH[2]}" percentage="${BASH_REMATCH[2]}"
# Generate random number between 1-100 and only respond if within percentage # Generate random number between 1-100 and only respond if within percentage
randomNum=$((RANDOM % 100 + 1)) randomNum=$((RANDOM % 100 + 1))
if [[ $randomNum -le $percentage ]]; then if [[ $randomNum -le $percentage ]]; then
eval "$command" execute_action "$command"
fi fi
else else
# No percentage specified, always respond # No percentage specified, always respond
eval "$keywordAction" execute_action "$keywordAction"
fi fi
lastWordMatch="${keywords[${w,,}]}" lastWordMatch="${keywords[${w,,}]}"
fi fi
done done
# Example of dealing with multi word triggers. # Process multi-word triggers (those starting with ~)
# Reset wordList without sorting it and with spaces removed.
wordList="$(echo "${@,,}" | tr -d '[:space:]')" wordList="$(echo "${@,,}" | tr -d '[:space:]')"
if [[ "${wordList,,}" =~ .*nowplaying:.* ]]; then for trigger in "${!keywords[@]}"; do
act "$chan" "$(shuf -n1 -e "cranks the volume up to 11" "got soooo high at that show" "boogies down to the sound of the band")!" if [[ "$trigger" =~ ^~ ]]; then
fi # Remove the ~ prefix for matching
triggerPattern="${trigger#\~}"
if [[ "${wordList,,}" =~ .*${triggerPattern}.* ]]; then
keywordAction="${keywords[$trigger]}"
# Check if the last element is a percentage
if [[ "$keywordAction" =~ (.*)\ ([0-9]+)%$ ]]; then
command="${BASH_REMATCH[1]}"
percentage="${BASH_REMATCH[2]}"
# Generate random number between 1-100 and only respond if within percentage
randomNum=$((RANDOM % 100 + 1))
if [[ $randomNum -le $percentage ]]; then
execute_action "$command"
fi
else
# No percentage specified, always respond
execute_action "$keywordAction"
fi
fi
fi
done

View File

@@ -12,10 +12,25 @@ fi
for l in $3 ; do for l in $3 ; do
text="${l#:}" text="${l#:}"
if [[ "${text}" =~ http://|https://|www\..* ]]; then if [[ "${text}" =~ http://|https://|www\..* ]]; then
pageTitle="$(curl -L -s --connect-timeout 5 "$text" | sed -n -e 'H;${x;s!.*<head[^>]*>\(.*\)</head>.*!\1!;T;s!.*<title>\(.*\)</title>.*!\1!p}' | w3m -dump -T text/html | tr '[:space:]' ' ')" # Security: Only allow http:// and https:// protocols
pageTitle="$(echo "$pageTitle" | tr -cd '[:print:]')" if [[ ! "$text" =~ ^https?:// ]]; then
if [[ ${#pageTitle} -gt 1 ]]; then # Convert www. to http://www.
msg "$2" "$pageTitle" if [[ "$text" =~ ^www\. ]]; then
fi text="http://$text"
else
# Skip unknown protocols
continue
fi
fi
# Remove potentially dangerous shell metacharacters from URL
text="${text//[;&|]/}"
# Fetch page title with timeout and security limits
pageTitle="$(curl -L -s --connect-timeout 5 --max-time 10 "$text" | sed -n -e 'H;${x;s!.*<head[^>]*>\(.*\)</head>.*!\1!;T;s!.*<title>\(.*\)</title>.*!\1!p}' | w3m -dump -T text/html | tr '[:space:]' ' ')"
pageTitle="$(echo "$pageTitle" | tr -cd '[:print:]')"
if [[ ${#pageTitle} -gt 1 ]]; then
msg "$2" "$pageTitle"
fi
fi fi
done done