fix: parse negative session IDs and add debug logging

Amolith created

The Mumble server can assign negative user session IDs (SIDs), e.g.,
`(-1)`. The regex patterns for parsing both move and disconnect events
have been updated to correctly handle negative session IDs.

Additionally, a new `log()` function provides granular debug output when
the `DEBUG` environment variable is set to a truthy value.

Change summary

CLAUDE.md      | 21 ++++++++++++++++++---
mumblingherald | 30 +++++++++++++++++++++++++++---
2 files changed, 45 insertions(+), 6 deletions(-)

Detailed changes

CLAUDE.md 🔗

@@ -54,10 +54,10 @@ The script is designed to run as a systemd service under a dedicated user accoun
 
 ## Log Format Parsing
 
-The script parses specific Mumble server log formats:
+The script parses specific Mumble server log formats. The user session ID, shown as `(SID)` below, can be a negative number (e.g. `(-1)`).
 
-- Move events: `<W>YYYY-MM-DD HH:MM:SS.mmm N => <ID:Username(N)> Moved Username:ID(N) to ChannelName[ID:N]`
-- Disconnect events: `<W>YYYY-MM-DD HH:MM:SS.mmm N => <ID:Username(N)> Connection closed: reason`
+- Move events: `<W>YYYY-MM-DD HH:MM:SS.mmm N => <UID:Username(SID)> Moved Username:UID(SID) to ChannelName[CID:N]`
+- Disconnect events: `<W>YYYY-MM-DD HH:MM:SS.mmm N => <UID:Username(SID)> Connection closed: reason`
 
 ## Message Templates
 
@@ -69,6 +69,21 @@ The herald's personality is expressed through varied message templates:
 - **Format**: Messages use `{USERNAME}` and `{CHANNEL}` placeholders for substitution
 - **Character actions**: Include physical actions like `*grumbles*`, `*adjusts spectacles*`, `*mutters*`
 
+## Debug Logging
+
+The script includes a `log()` function for debug output when the `DEBUG` environment variable is set to a truthy value (1, true, etc.):
+
+- **Function**: `log()` - Checks `if (($DEBUG))` and prints messages verbatim to stderr
+- **Usage**: Set `DEBUG=1` before running to see herald's internal thoughts
+- **Character**: Debug messages maintain the grumpy herald persona with observations like "_herald squints at parchment_" and "_mutters about modern technology_"
+- **Scope**: Added to helper functions (`is_monitored_channel`, `get_random_message`, `send_notification`, `parse_move_event`, `parse_disconnect_event`) but not main execution flow
+
+Example usage:
+
+```bash
+DEBUG=1 ./mumblingherald
+```
+
 ## Development Best Practices
 
 - Use shellcheck after every edit.

mumblingherald 🔗

@@ -19,14 +19,24 @@ MONITORED_CHANNELS=(
 	"Room 5"
 )
 
+# Function to print debug messages if DEBUG is true
+log() {
+	if ((DEBUG)); then
+		echo "$1" >&2
+	fi
+}
+
 # Function to check if a channel should be monitored
 is_monitored_channel() {
 	local channel="$1"
+	log "*herald squints at parchment* Checking if '$channel' is worth my attention..."
 	for monitored in "${MONITORED_CHANNELS[@]}"; do
 		if [[ "$channel" == "$monitored" ]]; then
+			log "*herald nods approvingly* Ah yes, '$channel' is on my sacred list!"
 			return 0
 		fi
 	done
+	log "*herald waves dismissively* '$channel'? Not my concern, I'm afraid."
 	return 1
 }
 
@@ -108,27 +118,34 @@ LEAVE_MESSAGES=(
 # Function to get a random message from an array
 get_random_message() {
 	local -n arr=$1
-	echo "${arr[$RANDOM % ${#arr[@]}]}"
+	local selected_index=$((RANDOM % ${#arr[@]}))
+	log "*herald rummages through dusty scroll collection* Selecting message ${selected_index} from ${#arr[@]} possibilities..."
+	echo "${arr[$selected_index]}"
 }
 
 # Function to send XMPP notification
 send_notification() {
 	local message="$1"
+	log "*herald clears throat and prepares speaking trumpet* Announcing: '$message'"
 	echo "$message" | go-sendxmpp -c -f "$XMPP_CONFIG_FILE" "$XMPP_RECIPIENTS"
+	log "*herald dusts off hands* Message dispatched to the digital realm!"
 }
 
 # Function to parse log line and extract user move information
 parse_move_event() {
 	local log_line="$1"
+	log "*herald peers at ancient log scroll* Examining movement event..."
 
 	# Extract user and destination channel
 	# Format: <W>2025-07-01 14:07:32.352 1 => <28:Amolith(5)> Moved Amolith:28(5) to Room 5[53:20]
-	if [[ "$log_line" =~ \<W\>[0-9-]+\ [0-9:.]+.*\>\ Moved\ ([^:]+):[0-9]+\([0-9]+\)\ to\ ([^\[]+)\[ ]]; then
+	if [[ "$log_line" =~ \<W\>[0-9-]+\ [0-9:.]+.*\>\ Moved\ ([^:]+):[0-9]+\(-?[0-9]+\)\ to\ ([^\[]+)\[ ]]; then
 		local username="${BASH_REMATCH[1]}"
 		local channel="${BASH_REMATCH[2]}"
+		log "*herald adjusts spectacles* Detected '$username' moving to '$channel'"
 
 		# Check if this channel should be monitored
 		if is_monitored_channel "$channel"; then
+			log "*herald perks up* This movement requires my attention!"
 			local join_msg
 			join_msg=$(get_random_message JOIN_MESSAGES)
 			local message="${join_msg//\{USERNAME\}/$username}"
@@ -136,23 +153,30 @@ parse_move_event() {
 			echo "*herald mumbles something about $username*"
 			send_notification "$message"
 		fi
+	else
+		log "*herald squints* Movement event format unrecognized. *mutters about modern technology* Line: $log_line"
 	fi
 }
 
 # Function to parse log line and extract user disconnect information
 parse_disconnect_event() {
 	local log_line="$1"
+	log "*herald listens for departing footsteps* Examining disconnect event..."
 
 	# Extract user from disconnect event
 	# Format: <W>2025-07-01 14:19:46.484 1 => <28:Amolith(5)> Connection closed: The remote host closed the connection [1]
-	if [[ "$log_line" =~ \<W\>[0-9-]+\ [0-9:.]+.*\<[0-9]+:([^\(]+)\([0-9]+\)\>\ Connection\ closed: ]]; then
+	# Format: <W>2025-07-01 23:24:47.986 1 => <199:DesertCr0w_Winblows(-1)> Connection closed: reason
+	if [[ "$log_line" =~ \<W\>[0-9-]+\ [0-9:.]+.*\<[0-9]+:([^\(]+)\(-?[0-9]+\)\>\ Connection\ closed: ]]; then
 		local username="${BASH_REMATCH[1]}"
+		log "*herald nods knowingly* '$username' has departed the realm entirely"
 
 		local leave_msg
 		leave_msg=$(get_random_message LEAVE_MESSAGES)
 		local message="${leave_msg//\{USERNAME\}/$username}"
 		echo "*herald notices someone's absence and sighs*"
 		send_notification "$message"
+	else
+		log "*herald frowns* Disconnect event format puzzling. *grumbles about log formats* Line: $log_line"
 	fi
 }