Some checks failed
Lint & Format / detect (push) Has been cancelled
Lint & Format / js-lint (push) Has been cancelled
Lint & Format / py-lint (push) Has been cancelled
Lint & Format / sh-lint (push) Has been cancelled
Lint & Format / go-lint (push) Has been cancelled
Monorepo Lint / lint-shell (push) Has been cancelled
Monorepo Lint / lint-js (push) Has been cancelled
RoadChain-SHA2048: f31122b68d27a309 RoadChain-Identity: alexa@sovereign RoadChain-Full: f31122b68d27a30949e6be04538b248fc34fc9a056bbb0cce1a6d2bcd333a83956b6c6bf6c4771ca9bb6fb6a284c367ebf6eee3d2c1d97ab6d2d5913fa4ae58c85045eabcef75c88329792905fa71b79e4f7b0d79616f32e99a806df1b0d1ad1e4abc1fb3ae950e91f79f029e0f17ed3463e5e5f05a7c81585955c3c8b8b50f8d10007d33237e1e87a601333aa33f6b48e14a6d1f78c40e178e7e3050b609668d2e323ee30df27dd63f3267dc46b08df2348aa4e8b64de024ff350c5191b04a15f588a43e0f1b6d97ef309ea6dc68e8e138a7060faff35fd3f1b38bcb702e49bea951f4e792cb4d2b7dd2a314b5eb72c4d350ceb9b29a2c9436e34192aee0e43
203 lines
7.0 KiB
Bash
Executable File
203 lines
7.0 KiB
Bash
Executable File
#!/bin/bash
|
|
# BlackRoad Network Welcome Center
|
|
# Watches for new devices joining the network and welcomes them to Slack
|
|
# Cron: */5 * * * * /Users/alexa/network-welcome.sh
|
|
|
|
set -e
|
|
|
|
KNOWN_DEVICES_FILE="$HOME/.blackroad/network/known-devices.json"
|
|
SLACK_WEBHOOK_URL="https://blackroad-slack.amundsonalexa.workers.dev/post"
|
|
SLACK_ASK_URL="https://blackroad-slack.amundsonalexa.workers.dev/ask"
|
|
LOG_FILE="$HOME/.blackroad/network/welcome.log"
|
|
|
|
mkdir -p "$(dirname "$KNOWN_DEVICES_FILE")"
|
|
|
|
# Initialize known devices file if missing
|
|
if [ ! -f "$KNOWN_DEVICES_FILE" ]; then
|
|
cat > "$KNOWN_DEVICES_FILE" << 'EOF'
|
|
{
|
|
"devices": {
|
|
"192.168.4.1": { "name": "Eero", "emoji": "📡", "type": "router", "welcomed": true },
|
|
"192.168.4.22": { "name": "Spark", "emoji": "⚡", "type": "iot", "welcomed": true },
|
|
"192.168.4.26": { "name": "BigScreen", "emoji": "📺", "type": "roku", "welcomed": true },
|
|
"192.168.4.27": { "name": "AppleTV", "emoji": "🍎", "type": "appletv", "welcomed": true },
|
|
"192.168.4.28": { "name": "Alexandria", "emoji": "📚", "type": "mac", "welcomed": true },
|
|
"192.168.4.33": { "name": "Streamer", "emoji": "🎬", "type": "roku", "welcomed": true },
|
|
"192.168.4.38": { "name": "Lucidia", "emoji": "💡", "type": "pi", "welcomed": true },
|
|
"192.168.4.44": { "name": "Pixel", "emoji": "🟢", "type": "iot", "welcomed": true },
|
|
"192.168.4.45": { "name": "Morse", "emoji": "📟", "type": "iot", "welcomed": true },
|
|
"192.168.4.49": { "name": "Alice", "emoji": "🌐", "type": "pi", "welcomed": true },
|
|
"192.168.4.96": { "name": "Cecilia", "emoji": "🧠", "type": "pi", "welcomed": true },
|
|
"192.168.4.98": { "name": "Aria", "emoji": "🎵", "type": "pi", "welcomed": true },
|
|
"192.168.4.101": { "name": "Octavia", "emoji": "🐙", "type": "pi", "welcomed": true }
|
|
},
|
|
"last_scan": ""
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
# Scan the network
|
|
ALIVE_IPS=$(nmap -sn 192.168.4.0/24 2>/dev/null | grep "Nmap scan report" | awk '{print $5}' | sort)
|
|
|
|
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
for ip in $ALIVE_IPS; do
|
|
# Check if this IP is known
|
|
IS_KNOWN=$(python3 -c "
|
|
import json,sys
|
|
with open('$KNOWN_DEVICES_FILE') as f:
|
|
d = json.load(f)
|
|
print('yes' if '$ip' in d.get('devices',{}) else 'no')
|
|
" 2>/dev/null)
|
|
|
|
if [ "$IS_KNOWN" = "no" ]; then
|
|
# NEW DEVICE DETECTED!
|
|
MAC=$(arp -a | grep "($ip)" | awk '{print $4}' | head -1)
|
|
MAC=${MAC:-"unknown"}
|
|
|
|
# Try to identify vendor
|
|
MAC_PREFIX=$(echo "$MAC" | cut -d: -f1-3)
|
|
VENDOR=$(curl -s "https://api.macvendors.com/$MAC_PREFIX" 2>/dev/null | head -1)
|
|
# Clean up vendor response
|
|
echo "$VENDOR" | grep -q "errors" && VENDOR="Unknown"
|
|
[ -z "$VENDOR" ] && VENDOR="Unknown"
|
|
|
|
# Check for open ports (quick)
|
|
PORTS=""
|
|
for port in 80 443 8060 22 5000 7000; do
|
|
(echo >/dev/tcp/$ip/$port) 2>/dev/null && PORTS="$PORTS $port"
|
|
done
|
|
|
|
# Detect device type
|
|
DEVICE_TYPE="unknown"
|
|
EMOJI="🆕"
|
|
SUGGESTED_NAME=""
|
|
|
|
# Roku?
|
|
ROKU_INFO=$(curl -s "http://$ip:8060/query/device-info" -m 2 2>/dev/null)
|
|
if echo "$ROKU_INFO" | grep -q "Roku"; then
|
|
DEVICE_TYPE="roku"
|
|
EMOJI="📺"
|
|
ROKU_MODEL=$(echo "$ROKU_INFO" | grep -o '<model-name>[^<]*' | sed 's/<model-name>//')
|
|
SUGGESTED_NAME="Roku-${ip##*.}"
|
|
fi
|
|
|
|
# Apple device?
|
|
if echo "$VENDOR" | grep -qi "apple"; then
|
|
DEVICE_TYPE="apple"
|
|
EMOJI="🍎"
|
|
SUGGESTED_NAME="Apple-${ip##*.}"
|
|
fi
|
|
|
|
# Raspberry Pi?
|
|
if echo "$VENDOR" | grep -qi "raspberry"; then
|
|
DEVICE_TYPE="pi"
|
|
EMOJI="🍓"
|
|
SUGGESTED_NAME="Pi-${ip##*.}"
|
|
fi
|
|
|
|
# Random MAC = phone/tablet
|
|
FIRST_BYTE=$(echo "$MAC" | cut -d: -f1)
|
|
IS_RANDOM=$(python3 -c "print('yes' if int('$FIRST_BYTE', 16) & 0x02 else 'no')" 2>/dev/null)
|
|
if [ "$IS_RANDOM" = "yes" ]; then
|
|
DEVICE_TYPE="mobile"
|
|
EMOJI="📱"
|
|
SUGGESTED_NAME="Mobile-${ip##*.}"
|
|
fi
|
|
|
|
# IoT (no ports, private MAC)
|
|
if [ -z "$PORTS" ] && [ "$DEVICE_TYPE" = "unknown" ]; then
|
|
DEVICE_TYPE="iot"
|
|
EMOJI="🔌"
|
|
SUGGESTED_NAME="IoT-${ip##*.}"
|
|
fi
|
|
|
|
[ -z "$SUGGESTED_NAME" ] && SUGGESTED_NAME="Device-${ip##*.}"
|
|
|
|
# Welcome message to Slack!
|
|
WELCOME_MSG="🎉 *NEW DEVICE DETECTED!*
|
|
|
|
${EMOJI} *${SUGGESTED_NAME}* just joined the network!
|
|
IP: \`$ip\`
|
|
MAC: \`$MAC\`
|
|
Vendor: $VENDOR
|
|
Type: $DEVICE_TYPE
|
|
Open ports:${PORTS:-" none"}
|
|
First seen: $NOW
|
|
|
|
_Welcome to the BlackRoad, friend. You're home now._"
|
|
|
|
curl -s -X POST "$SLACK_WEBHOOK_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"text\": $(echo "$WELCOME_MSG" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')}" \
|
|
>/dev/null 2>&1
|
|
|
|
# Have Hestia (hearth keeper) welcome them
|
|
curl -s -X POST "$SLACK_ASK_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"agent\": \"hestia\", \"message\": \"A new device just joined our home network: ${SUGGESTED_NAME} at ${ip} (${VENDOR}, type: ${DEVICE_TYPE}). Welcome them warmly in one sentence.\", \"slack\": true}" \
|
|
>/dev/null 2>&1
|
|
|
|
# Add to known devices
|
|
python3 -c "
|
|
import json
|
|
with open('$KNOWN_DEVICES_FILE', 'r') as f:
|
|
data = json.load(f)
|
|
data['devices']['$ip'] = {
|
|
'name': '$SUGGESTED_NAME',
|
|
'emoji': '$EMOJI',
|
|
'type': '$DEVICE_TYPE',
|
|
'mac': '$MAC',
|
|
'vendor': '$VENDOR',
|
|
'ports': '${PORTS}'.strip(),
|
|
'first_seen': '$NOW',
|
|
'welcomed': True
|
|
}
|
|
data['last_scan'] = '$NOW'
|
|
with open('$KNOWN_DEVICES_FILE', 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
" 2>/dev/null
|
|
|
|
echo "[$NOW] WELCOMED: $ip ($SUGGESTED_NAME, $VENDOR, $DEVICE_TYPE)" >> "$LOG_FILE"
|
|
fi
|
|
done
|
|
|
|
# Check for devices that went away (were known, now offline)
|
|
python3 << PYEOF
|
|
import json, subprocess, sys
|
|
|
|
with open("$KNOWN_DEVICES_FILE") as f:
|
|
data = json.load(f)
|
|
|
|
alive_set = set("$ALIVE_IPS".split())
|
|
gone = []
|
|
back = []
|
|
|
|
for ip, info in data["devices"].items():
|
|
was_online = info.get("online", True)
|
|
is_online = ip in alive_set
|
|
|
|
if was_online and not is_online and info.get("type") not in ("mobile",):
|
|
gone.append(f"{info.get('emoji','?')} *{info['name']}* ({ip})")
|
|
data["devices"][ip]["online"] = False
|
|
elif not was_online and is_online:
|
|
back.append(f"{info.get('emoji','?')} *{info['name']}* ({ip})")
|
|
data["devices"][ip]["online"] = True
|
|
|
|
with open("$KNOWN_DEVICES_FILE", "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
# Report comings and goings (only if something changed)
|
|
if gone:
|
|
msg = "👋 *Devices left the network:*\\n" + "\\n".join(f" {g}" for g in gone)
|
|
subprocess.run(["curl", "-s", "-X", "POST", "$SLACK_WEBHOOK_URL",
|
|
"-H", "Content-Type: application/json",
|
|
"-d", json.dumps({"text": msg})], capture_output=True)
|
|
|
|
if back:
|
|
msg = "🏠 *Welcome back!*\\n" + "\\n".join(f" {b}" for b in back)
|
|
subprocess.run(["curl", "-s", "-X", "POST", "$SLACK_WEBHOOK_URL",
|
|
"-H", "Content-Type: application/json",
|
|
"-d", json.dumps({"text": msg})], capture_output=True)
|
|
PYEOF
|