This commit is contained in:
Storm Dragon
2025-10-24 21:35:41 -04:00
parent 3669e07a9a
commit a0afefadfd
20 changed files with 533 additions and 223 deletions

View File

@@ -10,7 +10,9 @@ A simple, modular IRC bot written in bash
./bot.sh
```
2. Edit `bot.cfg` with your IRC server, channel, and bot settings
2. Edit `bot.cfg` with your IRC server, channels, and bot settings
- Configure multiple channels using an array: `channels=("channel1" "channel2")`
- Channel names should NOT include the # prefix
3. Run the bot again:
```bash

View File

@@ -1,5 +1,6 @@
#enter channels here in quotes before the )
channel="a11y"
# Enter channels here as an array. Add multiple channels like: channels=("a11y" "channel2" "channel3")
# Channel names should NOT include the # prefix
channels=("a11y")
# The date format for log entries. man date for details.
dateFormat='%B %d, %I:%m%P'
# Greet people who enter the channel? (true/false)

29
bot.sh
View File

@@ -30,7 +30,7 @@ done
# Variables important to modules need to be exported here.
export allowList
export channel
export channels
export input
export ignoreList
export nick
@@ -90,7 +90,10 @@ while true; do
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"
echo "JOIN #$channel" >> "$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
@@ -136,12 +139,17 @@ while true; do
;;
# for pings on nick/user
*"You have not"*)
echo "JOIN #$channel" | tee -a "$input"
for channelName in "${channels[@]}"; do
echo "JOIN #$channelName" | tee -a "$input"
done
;;
# Run on kick
:*!*@*" KICK "*" $nick :"*)
if [ "$autoRejoinChannel" = "true" ]; then
echo "JOIN #$channel" | tee -a "$input"
# 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%!*}"
@@ -197,7 +205,8 @@ while true; do
echo "Calling module ./modules/${command% *}/${command% *}/${command% *}.sh \"$who\" \"$from\" $willSanitized" >> "$log"
# Disable wildcards
set -f
"./modules/${command% *}/${command% *}.sh" "$who" "#$channel" "$will"
# For PMs, respond directly to the user, not to a channel
"./modules/${command% *}/${command% *}.sh" "$who" "$from" "$will"
# Enable wildcards
set +f
else
@@ -213,7 +222,7 @@ while true; do
who="${who:1}"
from="${result#*#}"
from="${from%% *}"
from="#${from:-$channel}"
from="#${from:-${channels[0]}}"
# Trigger stuff happens here.
# Call link trigger if msg contains a link:
if [[ "$result" =~ .*http://|https://|www\..* ]]; then
@@ -229,6 +238,8 @@ while true; do
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
@@ -245,6 +256,12 @@ while true; do
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

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env bash
[ -f functions.sh ] && source functions.sh
userNick="$1"
shift
chan="$1"
shift
snack="${@#botsnack}"
snack="${snack:-$(shuf -n1 -e\
"BBQ microchips" \
"BBQ sunflower seeds" \
"BBQ corn nuts" \
"deep fried goat placenta" \
"steak")} "
thanks="$(shuf -n1 -e "Thank you" "You're so awesome" "You shouldn't have" "You rock")"
favorite="$(shuf -n1 -e "my favorite" "yum yum" "this is bot heaven" "DELICIOUS")"
msg "$chan" "$thanks $userNick: $snack! $favorite!"

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
[ -f functions.sh ] && source functions.sh
# Add phrases in quotes to the array.
phrases=(
"cuss words, just let 'em roll, mother fucking shit god damn ass hole!"
"cuss words, just don't quit, mother fuck you damn shit head bitch!"
"damn!"
"fuck the fucking fuckers!"
"fuck the fuck off!"
"fuck!"
"fuck. fuck. fuck. Mother mother fuck. Mother mother fuck fuck. Mother fuck mother fuck. Noise noise noise."
"god damn it!"
"motherfucker"
"shit, piss, fuck, cunt, cocksucker, motherfucker, and tits."
"shit!"
"son of a bitch!"
)
msg "$2" "${phrases[$(($RANDOM % ${#phrases[@]}))]}"

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env bash
[ -f functions.sh ] && source functions.sh
douchebag=(
"Don't ask to ask, just ask!"
"STOP! READ THIS BEFORE YOU SPEAK! http://www.rockbox.org/wiki/IrcGuidelines"
"The human requesting this service can't be bothered to help you in person, so they requested a bot tell you that we don't discuss blah here, only blah+ which is totally different."
'Use a pastebin website, bun only the one approved by the users of this channel, else someone may flood your screen with whining and bitching!')
msg "$2" "$1: $(shuf -n1 -e "${douchebag[@]}")"

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
[ -f functions.sh ] && source functions.sh
# Dependencies required by this module
dependencies=("curl")
# Check dependencies before running
if ! check_dependencies "${dependencies[@]}"; then
msg "$2" "$1: This module requires: ${dependencies[*]}"
exit 1
fi
type="$(shuf -n1 -e "off" "you" "donut" "shakespeare" "linus" "king" "chainsaw" "madison")"
response="$(curl -s --connect-timeout 5 --max-time 10 -H "Accept: text/plain" "https://foaas.com/${type}/$3/$nick")"
if [[ -z "$response" || ${#response} -lt 5 ]]; then
msg "$2" "$1: Sorry, the FOAAS service is unavailable right now."
else
msg "$2" "$response"
fi

View File

@@ -1,33 +0,0 @@
#!/usr/bin/env bash
[ -f functions.sh ] && source functions.sh
shift
chan="$1"
shift
pimp() {
echo -n "$*" | sed \
-r -e "s/(^| )ask( |\?|\.|!)/\1aks\2/gI" \
-e "s/(^| )A /\1Uh $(shuf -e -n1 "god damn" "motha fuckin'") /gI" \
-e "s/(^| )I /\1Ah /gI" \
-e "s/(^| )is /\1be /gI" \
-e "s/(^| )are /\1is /gI" \
-e "s/(^| )(boy|dude|friend|guy|man)( |\?|\.|!)/\1$(shuf -n1 -e "bruh" "bruh-man" "brutha")\3/gI" \
-e "s/(^| )for( |\?|\.|!)/\1fuh\2/gI" \
-e "s/(^| )(appartment|house)( |\?|\.|!)/\1crib\3/gI" \
-e "s/(\w)ing( |\?|\.|!)/\1in'\2/gI" \
-e "s/(^| )my /\1mah /gI" \
-e "s/(^| )people /\1people /gI" \
-e "s/(^| )that( |\?|\.|!)/\1dat\2/gI" \
-e "s/(^| )this( |\?|\.|!)/\1dis\2/gI"
echo " $(shuf -n1 -e \
"Brace yourself foo'!" \
"What 'chew thinking Gee!" \
"and shit!" \
"sho 'nuff! ya'eard!?" \
"nd git Sheniquah's ass back ova' heeah!" \
)"
}
msg "$chan" "$(pimp "$*")"

View File

@@ -1,11 +0,0 @@
# The site on which this module relied is now down.
# This module is here in the hopes that it will one day come back.
# Thanks to joel.net for years of laughs.
[ -f functions.sh ] && source functions.sh
shift
chan="$1"
shift
pimpText="${*#pimp }"
echo "$chan" "$(curl -L -s --data-urlencode English="$pimpText" --data-urlencode submit="Submit send" http://joel.net/EBONICS/Translator | grep ' <textarea id="Ebonics" name="Ebonics" class="materialize-textarea validate" required>' | sed -e 's/^ <textarea id="Ebonics" name="Ebonics" class="materialize-textarea validate" required>//' -e "s/&#39;/'/g" -e 's#</textarea>##' -e 's/^$//g' -e 's/"/\\"/g')"

View File

@@ -1,56 +0,0 @@
#!/usr/bin/env bash
[ -f functions.sh ] && source functions.sh
shift
chan="$1"
shift
redneck() {
echo -n "$*" | sed \
-r -e "s/ass/ice/gI" \
-e "s/(^| )finger( |,|\?|\.|!|$)/\1fanger\2/gI" \
-e "s/(^| )thing( |,|\?|\.|!|$)/\1thang\2/gI" \
-e "s/(.\w+)ink(.*)/\1ank\2/gI" \
-e "s/(^| )A /\1uh /gI" \
-e "s/(.*)i([^a|^e|^u|^ble|^ck|^ft|^ll|^ndo|^on|^ps|^s|^v].*)/\1ah\2/gI" \
-e "s/(^| )I( |,|\?|\.|!|$)/\1Ah\2/g" \
-e "s/(^| )(boy|dude|fellow|guy|man)([?s])( |,|\?|\.|!|$)/\1feller\3\4/gI" \
-e "s/(^| )for( |,|\?|\.|!|$)/\1fer\2/gI" \
-e "s/(^| )(hello|hey|hi|how's it going|hows it going)( |,|\?|\.|!|$)/\1howdy\3/gI" \
-e "s/(^| )men( |,|\?|\.|!|$)/\1fellers\2/gI" \
-e "s/(^| )get( |,|\?|\.|!|$)/\1git\2/gI" \
-e "s/(^| )(appartment|cottage|house)( |,|\?|\.|!|$)/\1shack\3/gI" \
-e "s/(^| )(god damn|goddam)( |,|\?|\.|!|$)/\1gol-durn\3/gI" \
-e "s/(^| )damn( |,|\?|\.|!|$)/\1durn\2/gI" \
-e "s/(^| )(am not|is not|isn't|are not|aren't|will not)( |,|\?|\.|!|$)/\1ain't\3/gI" \
-e "s/(.*)backward(.*)/\1backerd\2/gI" \
-e "s/(^| )bear( |,|\?|\.|!|$)/\1bar\2/gI" \
-e "s/(^| )(cannot|can't)( |,|\?|\.|!|$)/\1cain't\3/gI" \
-e "s/(^| )careful( |,|\?|\.|!|$)/\1kerful\2/gI" \
-e "s/(^| )terrible( |,|\?|\.|!|$)/\1ter'ble\2/gI" \
-e "s/(\w)ing( |,|\?|\.|!|$)/\1in'\2/gI" \
-e "s/(\w)(i|ah)ght( |,|\?|\.|!|$)/\1aht'\3/gI" \
-e "s/(^| )my /\1mah /gI" \
-e "s/(^| )people( |,|\?|\.|!|$)/\1folks\2/gI" \
-e "s/(^| )pretty( |,|\?|\.|!|$)/\1purdy\2/gI" \
-e "s/(^| )sure( |,|\?|\.|!|$)/\1shore\2/gI" \
-e "s/(^| )there( |,|\?|\.|!|$)/\1thar'\2/gI" \
-e "s/(.*)window(.*)/\1windder\2/gI" \
-e "s/(.*)where|we're(.*)/\1wer\2/gI" \
-e "s/(^| )that( |,|\?|\.|!|$)/\1'at thar'\2/gI" \
-e "s/(^| )this( |,|\?|\.|!|$)/\1'is here\2/gI" \
-e "s/(^| )wash( |,|\?|\.|!|$)/\1wahrsh\2/gI" \
-e "s/(^| )([bg])ah([g|t])( |,|\?|\.|!|'|$)/\1\2i\3\4/gI" \
-e "s/(^| )aht( |,|\?|\.|!|'|$)/\1it\2/gI" \
-e "s/(^| )(ahf|if)( |,|\?|\.|!|$)/\1iffen\3/gI" \
-e "s/^[Yy]ou( |,|\.|\?\!|$)/Y'all\1/g"
echo " $(shuf -n1 -e \
"Hold Mah beer." \
"You're darn tootn" \
"Y'all come back now, ye'hear?" \
"Yyyyyyeeeeeeeeehaaaaaaaaawwwwww!" \
)"
}
msg "$chan" "$(redneck "$*")"

View File

@@ -335,7 +335,7 @@ case "$subcommand" in
# Set user's location
shift
if [[ $# -eq 0 ]]; then
msg "$channelName" "$name: Usage: weather set <location>"
msg "$channelName" "$name: Usage: weather -set <location>"
exit 0
fi

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# Word tracking leaderboard module - shows top users in a category
# shellcheck disable=SC1091
[ -f functions.sh ] && source functions.sh
# shellcheck disable=SC1091
[ -f triggers/wordtrack/categories.sh ] && source triggers/wordtrack/categories.sh
name="$1"
channelName="$2"
shift 2
category="$1"
# If no category specified, list available categories
# shellcheck disable=SC2154
if [[ -z "$category" ]]; then
msg "$channelName" "$name: Available categories: ${categories[*]}"
exit 0
fi
# Validate category exists
categoryValid=0
# shellcheck disable=SC2154
for cat in "${categories[@]}"; do
if [[ "$cat" == "$category" ]]; then
categoryValid=1
break
fi
done
if ((categoryValid == 0)); then
msg "$channelName" "$name: Invalid category. Available: ${categories[*]}"
exit 0
fi
# Data directory for this channel
dataDir="triggers/wordtrack/data/${channelName}"
if [[ ! -d "$dataDir" ]]; then
msg "$channelName" "$name: No tracking data available yet."
exit 0
fi
# Collect all users' counts for this category
declare -A leaderboard
for userFile in "$dataDir"/*.dat; do
[[ -f "$userFile" ]] || continue
userName=$(basename "$userFile" .dat)
while IFS='=' read -r cat count; do
if [[ "$cat" == "$category" ]]; then
leaderboard["$userName"]="$count"
fi
done < "$userFile"
done
# Check if anyone has been tracked
if [[ ${#leaderboard[@]} -eq 0 ]]; then
msg "$channelName" "$name: No one has been tracked in the ${category} category yet."
exit 0
fi
# Sort users by count (descending)
sortedUsers=()
while IFS= read -r line; do
sortedUsers+=("$line")
done < <(for user in "${!leaderboard[@]}"; do
echo "${leaderboard[$user]} $user"
done | sort -rn | head -5)
# Get level names for this category
levelsArrayName="${category}Levels"
declare -n levelsRef="$levelsArrayName"
# Build leaderboard message
leaderParts=()
position=1
for entry in "${sortedUsers[@]}"; do
count="${entry%% *}"
userName="${entry#* }"
# Find level for this count
level="Unranked"
for threshold in $(printf '%s\n' "${!levelsRef[@]}" | sort -n); do
if ((count >= threshold)); then
level="${levelsRef[$threshold]}"
fi
done
leaderParts+=("${position}. ${userName}: ${level} (${count} words)")
((position++))
done
unset -n levelsRef
msg "$channelName" "$name: Top ${category} users: $(IFS=' | '; echo "${leaderParts[*]}")"

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Word tracking stats module - shows user's current stats
# shellcheck disable=SC1091
[ -f functions.sh ] && source functions.sh
# shellcheck disable=SC1091
[ -f triggers/wordtrack/categories.sh ] && source triggers/wordtrack/categories.sh
name="$1"
channelName="$2"
shift 2
# Optional: check another user's stats
targetUser="${1:-$name}"
# User data file
dataDir="triggers/wordtrack/data/${channelName}"
userDataFile="${dataDir}/${targetUser}.dat"
# Check if user has any data
if [[ ! -f "$userDataFile" ]]; then
msg "$channelName" "$name: ${targetUser} has not been tracked yet."
exit 0
fi
# Load user data
declare -A userCounts
while IFS='=' read -r category count; do
userCounts["$category"]="$count"
done < "$userDataFile"
# Build stats message
statsMessage="${targetUser}'s word tracking stats: "
statsParts=()
# shellcheck disable=SC2154
for category in "${categories[@]}"; do
count="${userCounts[$category]:-0}"
if ((count > 0)); then
# Get current level for this category
levelsArrayName="${category}Levels"
declare -n levelsRef="$levelsArrayName"
currentLevel="Unranked"
nextThreshold=""
# Find current level and next threshold
for threshold in $(printf '%s\n' "${!levelsRef[@]}" | sort -n); do
if ((count >= threshold)); then
currentLevel="${levelsRef[$threshold]}"
elif [[ -z "$nextThreshold" ]]; then
nextThreshold="$threshold"
fi
done
unset -n levelsRef
# Build stat string
if [[ -n "$nextThreshold" ]]; then
remaining=$((nextThreshold - count))
statsParts+=("${category}: ${currentLevel} (${count}/${nextThreshold}, ${remaining} to next)")
else
statsParts+=("${category}: ${currentLevel} (MAX LEVEL - ${count} words)")
fi
fi
done
if [[ ${#statsParts[@]} -eq 0 ]]; then
msg "$channelName" "$name: ${targetUser} has not earned any levels yet."
else
statsMessage+=$(IFS=' | '; echo "${statsParts[*]}")
msg "$channelName" "$statsMessage"
fi

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
[ -f functions.sh ] && source functions.sh
# Dependencies required by this module
dependencies=("curl" "sed")
# Check dependencies before running
if ! check_dependencies "${dependencies[@]}"; then
msg "$2" "$1: This module requires: ${dependencies[*]}"
exit 1
fi
joke="$(curl -s --connect-timeout 5 --max-time 10 https://api.yomomma.info | sed -e 's/{"joke":"//' -e 's/"}$//')"
if [[ -z "$joke" || ${#joke} -lt 5 ]]; then
msg "$2" "$1: Sorry, couldn't fetch a yo momma joke right now."
else
joke="${joke//[[:space:]]/ }"
msg "$2" "$joke"
fi

View File

@@ -24,28 +24,7 @@ keywords[windows]="msg \"$chan\" \"$(shuf -n1 -e\
"Windows - Just another pain in the glass."\
"Windows, it's not pretty, it's not ugly, but it's pretty ugly.")!\""
keywords[emacs]="msg \"$chan\" \"$who, Real men of genius use vim!\""
keywords[eloquence]="msg \"$chan\" \"$(shuf -n1 -e \
"anticaesure" \
"caesure" \
"Goodhesville" \
"hh've" \
"Hoobhestank" \
"tzsche" \
"uncosp" \
"webhesday" \
"wedhesday")\""
keywords[eloquents]="msg \"$chan\" \"$(shuf -n1 -e \
"anticaesure" \
"caesure" \
"hh've" \
"tzsche" \
"uncosp" \
"webhesday" \
"wedhesday")\""
keywords[jaws]="msg \"$chan\" \"$(shuf -n1 -e \
"${who}: watch out for sharks!"\
"Ooooo! Jaws! Yeah, let's spend 1,500 bucks to buy what NVDA can do for free... Not much of an accountant are you ${who}?")\""
keywords[jfw]="msg \"$chan\" JFW: Acronym that means: Jaws! FUCKING WORTHLESS!"
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!\""
@@ -53,9 +32,6 @@ 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[chicken]="msg \"$chan\" \"$who, I'm gonna grab me $(shuf -n1 -e "a case of beer" "a weed eater" "a 5 gallon jug of vaseline" "a can of wd40") and a $(shuf -n1 -e dead frozen live young baby) chicken, and $(shuf -n1 -e "have fun" "make chicks" "lay it like an egg" "put my beak where it don't belong") ALL NIGHT LONG!!!\""
keywords[feather]="msg \"$chan\" \"$who: Erotic is using a feather. Kinky is using the whole chicken!!!\""
keywords[feathers]="msg \"$chan\" \"$who: Erotic is using a feather. Kinky is using the whole chicken!!!\""
keywords[dragonforce]="msg \"$chan\" \"$who: I love DragonForce!!!\""
keywords[vim]="msg \"$chan\" \"$(shuf -n1 -e \
"Praise vim! HA"\
@@ -76,9 +52,5 @@ done
# Reset wordList without sorting it and with spaces removed.
wordList="$(echo "${@,,}" | tr -d '[:space:]')"
if [[ "${wordList,,}" =~ .*nowplaying:.* ]]; then
if [ "$who" = "lilmike" ]; then
msg "$chan" "Ewww, it sounds like 2 robots making out!"
else
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")!"
fi
fi

View File

@@ -0,0 +1,103 @@
# Wordtrack Trigger
Automatically tracks word usage by users and awards level-ups based on configurable thresholds.
## How It Works
The wordtrack trigger monitors all channel messages and counts occurrences of tracked words across different categories. Users automatically level up when they reach configured thresholds.
## Files
- `wordtrack.sh` - Main trigger script (called automatically on messages)
- `categories.sh` - Configuration file defining categories, words, and levels
- `data/<channel>/<nick>.dat` - Per-user tracking data
## Modules
Users can interact with wordtrack using these command modules:
### `.wordtrack-stats [nick]`
Shows word tracking statistics for yourself or another user.
Example:
```
.wordtrack-stats
.wordtrack-stats alice
```
Output: `alice's word tracking stats: coffee: Coffee Lover (50/100, 50 to next) | tea: Tea Sipper (12/25, 13 to next)`
### `.wordtrack-leaders <category>`
Shows top 5 users in a category.
Example:
```
.wordtrack-leaders coffee
.wordtrack-leaders
```
Output: `Top coffee users: 1. alice: Coffee Lover (50 words) | 2. bob: Coffee Drinker (30 words) | 3. charlie: Coffee Newbie (15 words)`
If no category is provided, lists available categories.
## Configuration
Edit `categories.sh` to add new categories or modify existing ones.
### Adding a New Category
1. Create a word array: `categoryWords=("word1" "word2" "word3")`
2. Create a levels array: `declare -A categoryLevels=([threshold1]="Level Name" [threshold2]="Level Name")`
3. Add category to the categories list: `categories=("coffee" "tea" "yournewcategory")`
Example:
```bash
# Category: programming
programmingWords=("code" "coding" "python" "javascript" "rust" "git" "debug")
declare -A programmingLevels=(
[10]="Code Newbie"
[25]="Junior Dev"
[50]="Developer"
[100]="Senior Dev"
[200]="Code Wizard"
)
# Add to categories list
categories=("coffee" "tea" "gaming" "programming")
```
### Array Structure
- **Word arrays**: Simple indexed arrays containing words to track
- Words are matched case-insensitively
- Multiple word matches in one message count separately
- **Level arrays**: Associative arrays with threshold as key, level name as value
- Keys must be integers representing word counts
- Users advance when their count meets or exceeds the threshold
- Thresholds can be any positive integer
## Data Format
User data files (`data/<channel>/<nick>.dat`) use simple key=value format:
```
coffee=45
tea=12
gaming=78
```
## Integration with bot.sh
The wordtrack trigger is called automatically for all channel messages from users not in the ignoreList (bot.sh:254-262). It processes messages after the keywords trigger.
Level-up announcements are sent to the channel automatically when thresholds are crossed.
## Notes
- Users in the `ignoreList` are not tracked
- Word matching is case-insensitive
- Multiple occurrences of tracked words in a single message all count
- Data persists across bot restarts (stored in flat files)
- Each channel has independent tracking data

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Word tracking categories configuration
# Add your own categories by following the pattern below
# Category: coffee
# Words that trigger tracking for coffee category
coffeeWords=("coffee" "espresso" "latte" "mocha" "cappuccino" "americano" "frappuccino" "macchiato" "cortado" "affogato")
# Level thresholds and reward names for coffee category
# Array key is the threshold (word count needed), value is the level name
declare -A coffeeLevels=(
[10]="Coffee Newbie"
[25]="Coffee Drinker"
[50]="Coffee Lover"
[100]="Coffee Addict"
[200]="Coffee Fiend"
[500]="Coffee God"
)
# Category: tea
teaWords=("tea" "matcha" "chai" "oolong" "earl" "green tea" "black tea" "herbal" "chamomile" "rooibos")
declare -A teaLevels=(
[10]="Tea Sipper"
[25]="Tea Enthusiast"
[50]="Tea Connoisseur"
[100]="Tea Master"
[200]="Tea Guru"
)
# Category: gaming
gamingWords=("game" "gaming" "play" "played" "console" "steam" "xbox" "playstation" "nintendo" "pc gaming")
declare -A gamingLevels=(
[10]="Casual Gamer"
[25]="Regular Player"
[50]="Dedicated Gamer"
[100]="Hardcore Gamer"
[200]="Gaming Enthusiast"
[500]="Gaming Legend"
)
# Words that trigger tracking for drugs category
drugsWords=("kratom" "gummy" "hemp" "nicotine")
# Level thresholds and reward names for drugs category
# Array key is the threshold (word count needed), value is the level name
declare -A drugsLevels=(
[10]="Adict"
[20]="Junky"
[40]="Burnout"
[80]="Dope Fiend"
[160]="Intervention Candidate"
[320]="Drug Lord"
[640]="Pickled"
)
# List all active categories (must match the prefix of your arrays above)
# This is used by the trigger to know which categories to track
categories=("coffee" "tea" "gaming" "drugs")

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Word tracking categories configuration
# Add your own categories by following the pattern below
# Category: coffee
# Words that trigger tracking for coffee category
coffeeWords=("coffee" "espresso" "latte" "mocha" "cappuccino" "americano" "frappuccino" "macchiato" "cortado" "affogato")
# Level thresholds and reward names for coffee category
# Array key is the threshold (word count needed), value is the level name
declare -A coffeeLevels=(
[10]="Coffee Newbie"
[25]="Coffee Drinker"
[50]="Coffee Lover"
[100]="Coffee Addict"
[200]="Coffee Fiend"
[500]="Coffee God"
)
# Category: tea
teaWords=("tea" "matcha" "chai" "oolong" "earl" "green tea" "black tea" "herbal" "chamomile" "rooibos")
declare -A teaLevels=(
[10]="Tea Sipper"
[25]="Tea Enthusiast"
[50]="Tea Connoisseur"
[100]="Tea Master"
[200]="Tea Guru"
)
# Category: gaming
gamingWords=("game" "gaming" "play" "played" "console" "steam" "xbox" "playstation" "nintendo" "pc gaming")
declare -A gamingLevels=(
[10]="Casual Gamer"
[25]="Regular Player"
[50]="Dedicated Gamer"
[100]="Hardcore Gamer"
[200]="Gaming Enthusiast"
[500]="Gaming Legend"
)
# List all active categories (must match the prefix of your arrays above)
# This is used by the trigger to know which categories to track
categories=("coffee" "tea" "gaming")

View File

@@ -0,0 +1 @@
gaming=30

116
triggers/wordtrack/wordtrack.sh Executable file
View File

@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# Word tracking trigger - monitors messages and tracks word usage
# shellcheck disable=SC1091
[ -f functions.sh ] && source functions.sh
# shellcheck disable=SC1091
[ -f triggers/wordtrack/categories.sh ] && source triggers/wordtrack/categories.sh
name="$1"
channelName="$2"
shift 2
message="$*"
# Sanitize channel name (remove any IRC protocol remnants)
channelName="${channelName%%[[:space:]]*}"
channelName="${channelName//[^a-zA-Z0-9#_-]/}"
# Only process if we have a valid channel name starting with #
if [[ ! "$channelName" =~ ^#[a-zA-Z0-9_-]+$ ]]; then
exit 0
fi
# Convert message to lowercase for case-insensitive matching
messageLower="${message,,}"
# Create data directory for this channel if it doesn't exist
dataDir="triggers/wordtrack/data/${channelName}"
mkdir -p "$dataDir"
# User data file
userDataFile="${dataDir}/${name}.dat"
# Load existing user data
declare -A userCounts
if [[ -f "$userDataFile" ]]; then
while IFS='=' read -r category count; do
userCounts["$category"]="$count"
done < "$userDataFile"
fi
# Track which categories had level-ups
declare -a levelUps
# Process each category
# shellcheck disable=SC2154
for category in "${categories[@]}"; do
# Get the word array and levels array for this category
levelsArrayName="${category}Levels"
# Check if message contains any words from this category
wordCount=0
wordsArrayNameClean="${category}Words"
declare -n wordsRef="$wordsArrayNameClean"
for word in "${wordsRef[@]}"; do
# Count all occurrences of this word in the message
wordLower="${word,,}"
tempMessage="$messageLower"
while [[ "$tempMessage" =~ $wordLower ]]; do
((wordCount++))
# Remove the matched word to find more occurrences
tempMessage="${tempMessage/$wordLower/}"
done
done
unset -n wordsRef
# If words were found, update the counter
if ((wordCount > 0)); then
oldCount="${userCounts[$category]:-0}"
newCount=$((oldCount + wordCount))
userCounts["$category"]="$newCount"
# Check for level-up
oldLevel=""
newLevel=""
# Get the thresholds for this category using nameref
declare -n levelsRef="$levelsArrayName"
# Get old level
for threshold in $(printf '%s\n' "${!levelsRef[@]}" | sort -n); do
if ((oldCount >= threshold)); then
oldLevel="${levelsRef[$threshold]}"
fi
done
# Get new level
for threshold in $(printf '%s\n' "${!levelsRef[@]}" | sort -n); do
if ((newCount >= threshold)); then
newLevel="${levelsRef[$threshold]}"
fi
done
# If level changed, record the level-up
if [[ -n "$newLevel" && "$newLevel" != "$oldLevel" ]]; then
levelUps+=("$category:$newLevel:$newCount")
fi
# Clean up nameref
unset -n levelsRef
fi
done
# Save updated user data
: > "$userDataFile"
for category in "${!userCounts[@]}"; do
echo "${category}=${userCounts[$category]}" >> "$userDataFile"
done
# Announce level-ups
for levelUp in "${levelUps[@]}"; do
IFS=':' read -r category level count <<< "$levelUp"
msg "$channelName" "$name just leveled up in ${category}! You are now a ${level} with ${count} words!"
done