initial commit

Amolith created

Change summary

.claude/settings.local.json |   8 ++
README.md                   |  79 +++++++++++++++++++++
mumblingherald              | 146 +++++++++++++++++++++++++++++++++++++++
3 files changed, 233 insertions(+)

Detailed changes

README.md 🔗

@@ -0,0 +1,79 @@
+<!--
+SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+
+SPDX-License-Identifier: Unlicense
+-->
+
+# The Mumbling Herald
+
+A grumpy old herald who watches your Mumble server and announces comings and goings via XMPP.
+
+## Full setup
+
+- Create a herald user with minimal permissions
+  ```bash
+  sudo useradd -r -s /bin/false -d /var/lib/mumblingherald -m mumblingherald
+  sudo usermod -a -G mumble-server mumblingherald  # if mumble-server group exists for log access
+  ```
+- Copy script to user's directory, make it executable, and set ownership
+  ```bash
+  sudo cp mumblingherald /var/lib/mumblingherald
+  sudo chown mumblingherald:mumblingherald /var/lib/mumblingherald/mumblingherald
+  sudo chmod 700 /var/lib/mumblingherald/mumblingherald
+  ```
+- Edit the script to configure details
+  - `MUMBLE_LOG_FILE`: path to your Mumble server log
+  - `XMPP_RECIPIENTS`: recipient JID for notifications
+  - `MONITORED_CHANNELS`: display name of channels to watch
+- Install go-sendxmpp
+  - Download the latest binary from the [go-sendxmpp releases page](https://salsa.debian.org/mdosch/go-sendxmpp/-/releases)
+  - Extract and copy to system location
+    ```bash
+    sudo cp go-sendxmpp /usr/local/bin/
+    sudo chmod 755 /usr/local/bin/go-sendxmpp
+    ```
+- Create config directory
+  ```bash
+  sudo -u mumblingherald mkdir -p /var/lib/mumblingherald/.config/go-sendxmpp
+  ```
+- Create XMPP config in `/var/lib/mumblingherald/.config/go-sendxmpp/config` (edit values!)
+  ```bash
+  username: herald@your-xmpp-server.com
+  password: your-password
+  ```
+  - Correct permissions
+  ```bash
+  chmod 600 /var/lib/mumblingherald/.config/go-sendxmpp/config
+  ```
+- Create systemd service in `/etc/systemd/system/mumblingherald.service`
+  ```ini
+  [Unit]
+  Description=The Mumbling Herald - Mumble event notifier over XMPP
+  After=network.target
+  [Service]
+  Type=simple
+  User=mumblingherald
+  Group=mumble-server
+  WorkingDirectory=/var/lib/mumblingherald
+  Environment=HOME=/var/lib/mumblingherald
+  ExecStart=/var/lib/mumblingherald/mumblingherald
+  Restart=always
+  RestartSec=5
+  [Install]
+  WantedBy=multi-user.target
+  ```
+- Start the herald
+  ```bash
+  sudo systemctl daemon-reload
+  sudo systemctl enable --now mumblingherald
+  ```
+- Check status
+  ```bash
+  sudo systemctl status mumblingherald
+  ```
+
+## Logs
+
+```bash
+sudo journalctl -u mumblingherald -f
+```

mumblingherald 🔗

@@ -0,0 +1,146 @@
+#!/bin/bash
+
+# SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+#
+# SPDX-License-Identifier: Unlicense
+
+# TheMumblingHerald
+# Monitors Mumble server logs and sends XMPP notifications when users join specified channels
+
+# Configuration
+MUMBLE_LOG_FILE="/var/log/mumble-server/mumble-server.log"
+XMPP_CONFIG_FILE="$HOME/.config/go-sendxmpp/config"
+XMPP_RECIPIENTS="groupchat@conference.example.com"
+
+# List of channels to monitor (channel names)
+# Add or remove channels as needed
+MONITORED_CHANNELS=(
+	"Room 4"
+	"Room 5"
+)
+
+# Function to check if a channel should be monitored
+is_monitored_channel() {
+	local channel="$1"
+	for monitored in "${MONITORED_CHANNELS[@]}"; do
+		if [[ "$channel" == "$monitored" ]]; then
+			return 0
+		fi
+	done
+	return 1
+}
+
+# Arrays of varied messages for the mumbling herald
+# shellcheck disable=SC2034
+JOIN_MESSAGES=(
+	"➡️ {USERNAME} stumbles into {CHANNEL}"
+	"➡️ {USERNAME} wanders into {CHANNEL}"
+	"➡️ *clears throat* {USERNAME} appears in {CHANNEL}"
+	"➡️ {USERNAME} shuffles into {CHANNEL}"
+	"➡️ Behold! {USERNAME} manifests in {CHANNEL}"
+	"➡️ {USERNAME} finds their way to {CHANNEL}"
+	"➡️ Lo! {USERNAME} enters {CHANNEL}"
+	"➡️ {USERNAME} materializes in {CHANNEL}"
+)
+
+# shellcheck disable=SC2034
+LEAVE_MESSAGES=(
+	"⬅️ Oh, what? *ahem* {USERNAME} fled... somewhere *waves hand*"
+	"⬅️ {USERNAME} vanishes into the void..."
+	"⬅️ {USERNAME} disappeared! Just like that..."
+	"⬅️ *waves dismissively* Off goes {USERNAME}..."
+	"⬅️ Farewell to thee, {USERNAME}..."
+	"⬅️ {USERNAME} has made their exit..."
+	"⬅️ {USERNAME} has fled our realm..."
+	"⬅️ *sighs* And thus {USERNAME} departs..."
+)
+
+# Function to get a random message from an array
+get_random_message() {
+	local -n arr=$1
+	echo "${arr[$RANDOM % ${#arr[@]}]}"
+}
+
+# Function to send XMPP notification
+send_notification() {
+	local message="$1"
+	echo "$message" | go-sendxmpp -c -f "$XMPP_CONFIG_FILE" "$XMPP_RECIPIENTS"
+}
+
+# Function to parse log line and extract user move information
+parse_move_event() {
+	local log_line="$1"
+
+	# 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
+		local username="${BASH_REMATCH[1]}"
+		local channel="${BASH_REMATCH[2]}"
+
+		# Check if this channel should be monitored
+		if is_monitored_channel "$channel"; then
+			local join_msg
+			join_msg=$(get_random_message JOIN_MESSAGES)
+			local message="${join_msg//\{USERNAME\}/$username}"
+			message="${message//\{CHANNEL\}/$channel}"
+			echo "*herald mumbles something about $username*"
+			send_notification "$message"
+		fi
+	fi
+}
+
+# Function to parse log line and extract user disconnect information
+parse_disconnect_event() {
+	local log_line="$1"
+
+	# 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
+		local username="${BASH_REMATCH[1]}"
+
+		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"
+	fi
+}
+
+# Main monitoring loop
+echo "🗣️  Shaking the mumbling herald awake..."
+echo "📜 Herald will watch these sacred halls: ${MONITORED_CHANNELS[*]}"
+echo "👁️  Peering into the mystical log: $MUMBLE_LOG_FILE"
+echo "📨 Herald will announce to: $XMPP_RECIPIENTS"
+echo "🛑 Press Ctrl+C to put the herald back to sleep"
+echo
+
+# Check if log file exists
+if [[ ! -f "$MUMBLE_LOG_FILE" ]]; then
+	echo "💀 *herald panics* The sacred scroll is missing! $MUMBLE_LOG_FILE"
+	echo "🔧 Fix the MUMBLE_LOG_FILE path, good sir!"
+	exit 1
+fi
+
+# Check if go-sendxmpp is available
+if ! command -v go-sendxmpp &>/dev/null; then
+	echo "📯 *herald looks confused* Where's my speaking trumpet? (go-sendxmpp missing)"
+	echo "📦 Install go-sendxmpp so I can herald properly!"
+	exit 1
+fi
+
+# Check if XMPP config file exists
+if [[ ! -f "$XMPP_CONFIG_FILE" ]]; then
+	echo "⚠️  *herald squints* My address book seems to be missing: $XMPP_CONFIG_FILE"
+	echo "📝 Please inscribe your XMPP credentials!"
+fi
+
+# Start tailing the log file and processing events
+echo "👂 *herald puts ear to the wall and begins listening intently*"
+tail -F "$MUMBLE_LOG_FILE" | while read -r line; do
+	# Process lines that contain "Moved" or "Connection closed" events
+	if [[ "$line" == *"Moved"* ]]; then
+		parse_move_event "$line"
+	elif [[ "$line" == *"Connection closed"* ]]; then
+		parse_disconnect_event "$line"
+	fi
+done