Compare commits

...

9 Commits

Author SHA1 Message Date
Storm Dragon
2a25355c58 Accidently pushed the non-example categories.sh file. 2025-10-27 19:34:17 -04:00
Storm Dragon
fbce315cf6 Fixed word boundary matching and made wildcards possible. 2025-10-27 19:31:02 -04:00
Storm Dragon
d53c44cc76 Fix parsing for real this time. 2025-10-27 15:14:14 -04:00
Storm Dragon
2cd8f3e99d Improved zipcode parsing. 2025-10-27 14:55:43 -04:00
Storm Dragon
9b7a786a96 Attempted fixes for weather. 2025-10-27 14:39:46 -04:00
Storm Dragon
d6825c4a92 Fixed the extra : in jokes file. 2025-10-27 01:02:20 -04:00
Storm Dragon
278acc4d8f Removed the broken fml module. 2025-10-27 00:43:39 -04:00
Storm Dragon
b61d6c673c Fixed the exit command to actually exit instead of trying to reconnect. 2025-10-25 17:02:55 -04:00
Storm Dragon
259195b021 More updates to advice module. 2025-10-25 03:30:20 -04:00
8 changed files with 215 additions and 111 deletions

10
bot.sh
View File

@@ -76,6 +76,7 @@ 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")
@@ -129,6 +130,9 @@ trim_log() {
# 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
@@ -334,6 +338,12 @@ while true; do
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

View File

@@ -24,6 +24,7 @@ check_dependencies() {
}
close_bot() {
intentionalExit=true
echo -en "QUIT :${quitMessage}\r\n" >> "$input"
}

View File

@@ -3,6 +3,7 @@
advice=(
'If love hurts, move slightly to the left.'
"Keep smiling. It confuses the autopsy report."
'When in doubt, mumble.'
"Some people are like clouds, when they disappear it's a beautiful day."
"Proof that light travels faster than sound? Some people appear bright until they speak."

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
[ -f functions.sh ] && source functions.sh
# Dependencies required by this module
dependencies=("curl" "w3m" "grep" "iconv")
# Check dependencies before running
if ! check_dependencies "${dependencies[@]}"; then
msg "$2" "$1: This module requires: ${dependencies[*]}"
exit 1
fi
fml="$(curl -Ls --connect-timeout 5 https://fmylife.com/random | grep -m1 -A1 '<a class="article-link" href="/article/.*>' | tail -1 | w3m -dump -T text/html | iconv -f utf-8 -t ascii | tr '[:space:]' ' ')"
if [[ ${#fml} -gt 10 ]]; then
msg "$2" "${fml}"
else
msg "$2" "I couldn't get any fmls. fml"
fi

View File

@@ -7,7 +7,7 @@ lastLine="$(grep -n '^exit 0' $0)"
lastLine=${lastLine%%:*}
lastLine=$((lastLine + 1))
# display a line from the end of the file after the script
msg "$2" "$(tail +${lastLine} "$0" | sed '/^$/d' | shuf -n1)":
msg "$2" "$(tail +${lastLine} "$0" | sed '/^$/d' | shuf -n1)"
# Put jokes after the exit, one per line.
exit 0

View File

@@ -15,8 +15,16 @@ name="$1"
channelName="$2"
shift 2
# Parse arguments into array to handle subcommands properly
# When bot.sh calls this with "$will", it arrives as a single quoted argument
# e.g., "set Ferguson North Carolina" comes as one $1, not multiple args
# shellcheck disable=SC2206
request=($*)
# Database file for user locations and cached geocoding
weatherDb="data/weather.db"
weatherDbLock="${weatherDb}.lock"
nominatimRateLimitFile="data/nominatim_last_request"
mkdir -p "$(dirname "$weatherDb")"
touch "$weatherDb"
@@ -73,24 +81,66 @@ save_user_location() {
local lon="$4"
local formatted="$5"
# Remove old entry if exists
if [[ -f "$weatherDb" ]]; then
grep -v "^USER|${user}|" "$weatherDb" > "${weatherDb}.tmp"
mv "${weatherDb}.tmp" "$weatherDb"
fi
# Use flock to prevent race conditions
(
flock -x 200
# Add new entry
echo "USER|${user}|${location}|${lat}|${lon}|${formatted}" >> "$weatherDb"
# Remove old entry if exists
if [[ -f "$weatherDb" ]]; then
grep -v "^USER|${user}|" "$weatherDb" > "${weatherDb}.tmp"
mv "${weatherDb}.tmp" "$weatherDb"
fi
# Add new entry
echo "USER|${user}|${location}|${lat}|${lon}|${formatted}" >> "$weatherDb"
) 200>"$weatherDbLock"
}
# Function to delete user location
delete_user_location() {
local user="$1"
if [[ -f "$weatherDb" ]]; then
grep -v "^USER|${user}|" "$weatherDb" > "${weatherDb}.tmp"
mv "${weatherDb}.tmp" "$weatherDb"
fi
# Use flock to prevent race conditions
(
flock -x 200
if [[ -f "$weatherDb" ]]; then
grep -v "^USER|${user}|" "$weatherDb" > "${weatherDb}.tmp"
mv "${weatherDb}.tmp" "$weatherDb"
fi
) 200>"$weatherDbLock"
}
# Function to maintain cache size
maintain_cache() {
# Use flock to prevent race conditions
(
flock -x 200
if [[ ! -f "$weatherDb" ]]; then
return 0
fi
local lineCount
lineCount=$(wc -l < "$weatherDb")
# If file has 2000 or more lines, keep only the most recent 1500
if [[ $lineCount -ge 2000 ]]; then
# Separate USER and CACHE entries
grep "^USER|" "$weatherDb" > "${weatherDb}.tmp.user" 2>/dev/null || true
grep "^CACHE|" "$weatherDb" | tail -n 1000 > "${weatherDb}.tmp.cache" 2>/dev/null || true
# Combine them back (keep all user entries, trim cache entries)
cat "${weatherDb}.tmp.user" "${weatherDb}.tmp.cache" > "${weatherDb}.tmp" 2>/dev/null
mv "${weatherDb}.tmp" "$weatherDb"
# Clean up temp files
rm -f "${weatherDb}.tmp.user" "${weatherDb}.tmp.cache"
fi
) 200>"$weatherDbLock"
}
# Function to cache geocode result
@@ -106,10 +156,45 @@ cache_location() {
local cached
cached=$(get_cached_location "$query")
if [[ -z "$cached" ]]; then
echo "CACHE|${queryLower}|${location}|${lat}|${lon}|${formatted}" >> "$weatherDb"
# Use flock to prevent race conditions
(
flock -x 200
echo "CACHE|${queryLower}|${location}|${lat}|${lon}|${formatted}" >> "$weatherDb"
) 200>"$weatherDbLock"
# Maintain cache size after adding new entry
maintain_cache
fi
}
# Function to enforce Nominatim rate limit (1 request per second)
rate_limit_nominatim() {
local rateLimitLock="${nominatimRateLimitFile}.lock"
(
flock -x 200
# Check if rate limit file exists and read last request time
if [[ -f "$nominatimRateLimitFile" ]]; then
local lastRequest
lastRequest=$(cat "$nominatimRateLimitFile" 2>/dev/null || echo "0")
local currentTime
currentTime=$(date +%s)
local timeSinceLastRequest=$((currentTime - lastRequest))
# If less than 1 second has passed, sleep for the remaining time
if [[ $timeSinceLastRequest -lt 1 ]]; then
local sleepTime=$((1 - timeSinceLastRequest))
sleep "$sleepTime"
fi
fi
# Update last request time
date +%s > "$nominatimRateLimitFile"
) 200>"$rateLimitLock"
}
# Function to format location (City, State or City, Country)
format_location() {
local address="$1"
@@ -176,12 +261,45 @@ geocode_location() {
local userAgent="stormbot-weather/1.0"
local response
# URL encode the query (replace spaces with +)
local encodedQuery="${query// /+}"
# Check if query contains common US indicators (case-insensitive)
local countryCode=""
local queryLower="${query,,}"
local isUsZipCode=false
# Common US state names and abbreviations
local usStates="alabama|alaska|arizona|arkansas|california|colorado|connecticut|delaware|florida|georgia|hawaii|idaho|illinois|indiana|iowa|kansas|kentucky|louisiana|maine|maryland|massachusetts|michigan|minnesota|mississippi|missouri|montana|nebraska|nevada|new hampshire|new jersey|new mexico|new york|north carolina|north dakota|ohio|oklahoma|oregon|pennsylvania|rhode island|south carolina|south dakota|tennessee|texas|utah|vermont|virginia|washington|west virginia|wisconsin|wyoming"
# Check if this is a US ZIP code query
if [[ "$query" =~ ^[0-9]{5}([[:space:]]|$) ]]; then
isUsZipCode=true
fi
if [[ "$queryLower" =~ (usa|united states) ]] || \
[[ "$query" =~ [[:space:]][A-Z]{2}$ ]] || \
[[ "$queryLower" =~ [[:space:]](${usStates})$ ]] || \
[[ "$isUsZipCode" == true ]]; then
# Query mentions USA, ends with state abbreviation, ends with state name, or is a zip code
countryCode="&countrycodes=us"
fi
# Build the query URL
local apiUrl
if [[ "$isUsZipCode" == true ]]; then
# For ZIP codes, use postalcode parameter for better accuracy
local zipCode="${query%% *}" # Extract just the ZIP code (remove any trailing text)
apiUrl="${url}?postalcode=${zipCode}&country=us&format=json&limit=1&addressdetails=1"
else
# URL encode the query (replace spaces with +)
local encodedQuery="${query// /+}"
apiUrl="${url}?q=${encodedQuery}&format=json&limit=1&addressdetails=1${countryCode}"
fi
# Enforce rate limit before making API request
rate_limit_nominatim
response=$(curl -s --connect-timeout 5 --max-time 10 \
-H "User-Agent: ${userAgent}" \
"${url}?q=${encodedQuery}&format=json&limit=1&addressdetails=1")
"${apiUrl}")
if [[ -z "$response" || "$response" == "[]" ]]; then
return 1
@@ -217,7 +335,7 @@ get_weather() {
local url="https://api.open-meteo.com/v1/forecast"
local params="latitude=${lat}&longitude=${lon}"
params+="&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m"
params+="&current=temperature_2m,apparent_temperature,relative_humidity_2m,weather_code,wind_speed_10m"
params+="&daily=weather_code,temperature_2m_max,temperature_2m_min"
params+="&timezone=auto&forecast_days=3&temperature_unit=fahrenheit&wind_speed_unit=mph"
@@ -239,17 +357,35 @@ format_weather() {
# Parse current weather
local temp
local feelsLike
local humidity
local windSpeed
local weatherCode
temp=$(echo "$weatherData" | jq -r '.current.temperature_2m // "N/A"')
feelsLike=$(echo "$weatherData" | jq -r '.current.apparent_temperature // "N/A"')
humidity=$(echo "$weatherData" | jq -r '.current.relative_humidity_2m // "N/A"')
windSpeed=$(echo "$weatherData" | jq -r '.current.wind_speed_10m // "N/A"')
weatherCode=$(echo "$weatherData" | jq -r '.current.weather_code // 0')
local conditions="${weatherCodes[$weatherCode]:-Unknown}"
# Build message
local message="Weather for ${locationName}: ${temp}°F, ${conditions}"
local message="Weather for ${locationName}: ${temp}°F"
# Add feels-like if different from actual temp (round to nearest degree for comparison)
if [[ "$feelsLike" != "N/A" && "$temp" != "N/A" ]]; then
local tempRounded
local feelsLikeRounded
tempRounded=$(printf "%.0f" "$temp" 2>/dev/null || echo "$temp")
feelsLikeRounded=$(printf "%.0f" "$feelsLike" 2>/dev/null || echo "$feelsLike")
# Only show feels-like if it differs by at least 3 degrees
local diff=$((tempRounded - feelsLikeRounded))
if [[ ${diff#-} -ge 3 ]]; then
message+=" (feels like ${feelsLike}°F)"
fi
fi
message+=", ${conditions}"
if [[ "$humidity" != "N/A" ]]; then
message+=", Humidity: ${humidity}%"
@@ -323,23 +459,20 @@ format_forecast() {
}
# Main command logic
subcommand="${1:-}"
# If subcommand is the module name itself (w or weather), treat as no argument
if [[ "$subcommand" == "w" || "$subcommand" == "weather" ]]; then
subcommand=""
fi
# Check if first element is a subcommand
subcommand="${request[0]:-}"
case "$subcommand" in
set)
# Set user's location
shift
if [[ $# -eq 0 ]]; then
msg "$channelName" "$name: Usage: weather -set <location>"
# Remove 'set' from the array and get the rest as location
if [[ ${#request[@]} -lt 2 ]]; then
msg "$channelName" "$name: Usage: weather set <location>"
exit 0
fi
location="$*"
# Get everything after 'set' as the location
location="${request[*]:1}"
# Validate location length
if [[ ${#location} -gt 100 ]]; then
@@ -364,6 +497,7 @@ case "$subcommand" in
save_user_location "$name" "$fullAddress" "$lat" "$lon" "$formattedLocation"
msg "$channelName" "$name: Your location has been set to: ${formattedLocation}"
exit 0
;;
del|delete)
@@ -376,7 +510,7 @@ case "$subcommand" in
# Show weather (default action)
if [[ -n "$subcommand" ]]; then
# Weather for specified location
location="$*"
location="${request[*]}"
# Validate location length
if [[ ${#location} -gt 100 ]]; then

View File

@@ -1,61 +0,0 @@
#!/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

@@ -69,10 +69,48 @@ for category in "${categories[@]}"; do
# Count all occurrences of this word in the message
wordLower="${word,,}"
tempMessage="$messageLower"
while [[ "$tempMessage" =~ $wordLower ]]; do
# Parse wildcard markers to determine match type
# * prefix = allow match at end of larger word (e.g., *game matches endgame)
# * suffix = allow match at start of larger word (e.g., coffee* matches coffeehouse)
# both = allow match anywhere (e.g., *play* matches gameplay)
# none = exact word match only (e.g., tea only matches tea)
prefixWild=false
suffixWild=false
if [[ "$wordLower" == \** ]]; then
prefixWild=true
wordLower="${wordLower#\*}"
fi
if [[ "$wordLower" == *\* ]]; then
suffixWild=true
wordLower="${wordLower%\*}"
fi
# Build regex pattern based on wildcard markers
if $prefixWild && $suffixWild; then
# Match anywhere in text
pattern="$wordLower"
elif $prefixWild; then
# Match at end of word or standalone
pattern="$wordLower([[:space:][:punct:]]|$)"
elif $suffixWild; then
# Match at start of word or standalone
pattern="(^|[[:space:][:punct:]])$wordLower"
else
# Exact word match only
pattern="(^|[[:space:][:punct:]])$wordLower([[:space:][:punct:]]|$)"
fi
# Use word boundary matching based on pattern
while [[ "$tempMessage" =~ $pattern ]]; do
((wordCount++))
# Remove the matched word to find more occurrences
tempMessage="${tempMessage/$wordLower/}"
# Use the actual matched portion to avoid removing partial matches
matchedWord="${BASH_REMATCH[0]}"
tempMessage="${tempMessage/"$matchedWord"/ }"
done
done