Compare commits

...

14 Commits

Author SHA1 Message Date
SpudGunMan
0c6fcf10ef Update README.md 2025-11-01 09:39:08 -07:00
SpudGunMan
647ae92649 cleanup 2025-11-01 00:49:33 -07:00
SpudGunMan
254eef4be9 fix No Data 2025-10-31 20:30:38 -07:00
SpudGunMan
bd0a94e2a1 refactor ✈️
add better altitude detector
2025-10-31 20:06:56 -07:00
SpudGunMan
2d8256d9f7 Update test_bot.py 2025-10-31 17:25:20 -07:00
SpudGunMan
1f9b81865e Update test_bot.py 2025-10-31 17:15:30 -07:00
SpudGunMan
17221cf37f enhance auto-block
with string protectors
2025-10-31 16:19:13 -07:00
SpudGunMan
47dd75bfb3 autoBlock, enhance ban list and such
https://github.com/SpudGunMan/meshing-around/issues/252

# Enable or disable automatic banning of nodes
autoBanEnabled = False
2025-10-31 16:02:14 -07:00
SpudGunMan
d4773705ce echo welcome! 2025-10-31 13:24:44 -07:00
SpudGunMan
4f46e659d9 TOC updated 2025-10-31 12:41:52 -07:00
SpudGunMan
404f84f39c echo motd 2025-10-31 12:39:19 -07:00
SpudGunMan
c07ec534a7 Update mesh_bot.py 2025-10-31 12:33:38 -07:00
SpudGunMan
4d88aed0d8 🐬
enhance echo with admin functions
2025-10-31 12:04:52 -07:00
SpudGunMan
b1946608f4 FIX👀
how the heck did I miss this!
2025-10-31 11:42:33 -07:00
8 changed files with 345 additions and 106 deletions

View File

@@ -481,3 +481,9 @@ DEBUGpacket = False
# metaPacket detailed logging, the filter negates the port ID
debugMetadata = False
metadataFilter = TELEMETRY_APP,POSITION_APP
# Enable or disable automatic banning of nodes
autoBanEnabled = False
# Number of offenses before auto-ban
autoBanThreshold = 5
# Timeframe for offenses (in seconds)
autoBanTimeframe = 3600

View File

@@ -249,7 +249,11 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
global multiPing
myNodeNum = globals().get(f'myNodeNum{deviceID}', 777)
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag"
pingHelp = "🤖Ping Command Help:\n" \
"🏓 Send 'ping' or 'ack' or 'test' to get a response.\n" \
"🏓 Send 'ping <number>' to get multiple pings in DM"
"🏓 ping @USERID to send a Joke from the bot"
return pingHelp
msg = ""
type = ''
@@ -330,8 +334,11 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
# no autoping in channels
pingCount = 1
if pingCount > 51:
if pingCount > 51 and pingCount <= 101:
pingCount = 50
if pingCount > 800:
ban_hammer(message_from_id, deviceID, reason="Excessive auto-ping request")
return "🚫⛔auto-ping request denied."
except ValueError:
pingCount = -1
@@ -358,7 +365,8 @@ def handle_emergency(message_from_id, deviceID, message):
# if user in bbs_ban_list return
if str(message_from_id) in my_settings.bbs_ban_list:
# silent discard
logger.warning(f"System: {message_from_id} on spam list, no emergency responder alert sent")
hammer_value = ban_hammer(message_from_id, deviceID, reason="Emergency Alert from banned node")
logger.warning(f"System: {message_from_id} on spam list, no emergency responder alert sent. Ban hammer value: {hammer_value}")
return ''
# trgger alert to emergency_responder_alert_channel
if message_from_id != 0:
@@ -390,11 +398,42 @@ def handle_motd(message, message_from_id, isDM):
return msg
def handle_echo(message, message_from_id, deviceID, isDM, channel_number):
# Check if user is admin
isAdmin = isNodeAdmin(message_from_id)
# Admin extended syntax: echo <string> c=<channel> d=<device>
if isAdmin and message.strip().lower().startswith("echo ") and not message.strip().endswith("?"):
msg_to_echo = message.split(" ", 1)[1]
target_channel = channel_number
target_device = deviceID
# Split into words to find c= and d=, but preserve spaces in message
words = msg_to_echo.split()
new_words = []
for w in words:
if w.startswith("c=") and w[2:].isdigit():
target_channel = int(w[2:])
elif w.startswith("d=") and w[2:].isdigit():
target_device = int(w[2:])
else:
new_words.append(w)
msg_to_echo = " ".join(new_words).strip()
# Replace motd/MOTD with the current MOTD from settings
msg_to_echo = " ".join(my_settings.MOTD if w.lower() == "motd" else w for w in msg_to_echo.split())
# Replace welcome! with the current welcome_message from settings
msg_to_echo = " ".join(my_settings.welcome_message if w.lower() == "welcome!" else w for w in msg_to_echo.split())
# Send echo to specified channel/device
logger.debug(f"System: Admin Echo to channel {target_channel} device {target_device} message: {msg_to_echo}")
time.sleep(splitDelay) # throttle for 2x send
send_message(msg_to_echo, target_channel, 0, target_device)
time.sleep(splitDelay) # throttle for 2x send
return f"🐬echoed to channel {target_channel} device {target_device}"
# dev echoBinary off
echoBinary = False
if echoBinary:
try:
#send_raw_bytes echo the data to the channel with synch word:
port_num = 256
synch_word = b"echo:"
parts = message.split("echo ", 1)
@@ -403,25 +442,29 @@ def handle_echo(message, message_from_id, deviceID, isDM, channel_number):
raw_bytes = synch_word + msg_to_echo.encode('utf-8')
send_raw_bytes(message_from_id, raw_bytes, nodeInt=deviceID, channel=channel_number, portnum=port_num)
return f"Sent binary echo message to {message_from_id} to {port_num} on channel {channel_number} device {deviceID}"
else:
return "Please provide a message to echo back to you. Example:echo Hello World"
except Exception as e:
logger.error(f"System: Echo Exception {e}")
return f"Sent binary echo message to {message_from_id} to {port_num} on channel {channel_number} device {deviceID}"
if "?" in message.lower():
return "command returns your message back to you. Example:echo Hello World"
elif "echo " in message.lower():
parts = message.lower().split("echo ", 1)
if "?" in message:
isAdmin = isNodeAdmin(message_from_id)
if isAdmin:
return (
"Admin usage: echo <message> c=<channel> d=<device>\n"
"Example: echo Hello world c=1 d=2"
)
return "command returns your message back to you. Example: echo Hello World"
# process normal echo back to user
elif message.strip().lower().startswith("echo "):
parts = message.split("echo ", 1)
if len(parts) > 1 and parts[1].strip() != "":
echo_msg = parts[1]
if channel_number != my_settings.echoChannel and not isDM:
echo_msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + echo_msg
return echo_msg
else:
return "Please provide a message to echo back to you. Example:echo Hello World"
else:
return "Please provide a message to echo back to you. Example:echo Hello World"
return "Please provide a message to echo back to you. Example: echo Hello World"
return "🐬echo.."
def handle_wxalert(message_from_id, deviceID, message):
if my_settings.use_meteo_wxApi:
@@ -1614,6 +1657,10 @@ def handle_boot(mesh=True):
if my_settings.useDMForResponse:
logger.debug("System: Respond by DM only")
if my_settings.autoBanEnabled:
logger.debug(f"System: Auto-Ban Enabled for {my_settings.autoBanThreshold} messages in {my_settings.autoBanTimeframe} seconds")
load_bbsBanList()
if my_settings.log_messages_to_file:
logger.debug("System: Logging Messages to disk")
if my_settings.syslog_to_file:
@@ -1746,9 +1793,14 @@ def onReceive(packet, interface):
message_from_id = packet['from']
# if message_from_id is not in the seenNodes list add it
if not any(node['nodeID'] == message_from_id for node in seenNodes):
seenNodes.append({'nodeID': message_from_id, 'rxInterface': rxNode, 'channel': channel_number, 'welcome': False, 'lastSeen': time.time()})
if not any(node.get('nodeID') == message_from_id for node in seenNodes):
seenNodes.append({'nodeID': message_from_id, 'rxInterface': rxNode, 'channel': channel_number, 'welcome': False, 'first_seen': time.time(), 'lastSeen': time.time()})
else:
# update lastSeen time
for node in seenNodes:
if node.get('nodeID') == message_from_id:
node['lastSeen'] = time.time()
break
# BBS DM MAIL CHECKER
if bbs_enabled and 'decoded' in packet:
msg = bbs_check_dm(message_from_id)
@@ -1757,7 +1809,12 @@ def onReceive(packet, interface):
message = "Mail: " + msg[1] + " From: " + get_name_from_number(msg[2], 'long', rxNode)
bbs_delete_dm(msg[0], msg[1])
send_message(message, channel_number, message_from_id, rxNode)
# CHECK with ban_hammer() if the node is banned
if str(message_from_id) in my_settings.bbs_ban_list or str(message_from_id) in my_settings.autoBanlist:
logger.warning(f"System: Banned Node {message_from_id} tried to send a message. Ignored. Try adding to node firmware-blocklist")
return
# handle TEXT_MESSAGE_APP
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
@@ -1837,7 +1894,7 @@ def onReceive(packet, interface):
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism} Hostname:{rxNodeHostName}")
# check with stringSafeChecker if the message is safe
if stringSafeCheck(message_string) is False:
if stringSafeCheck(message_string, message_from_id) is False:
logger.warning(f"System: Possibly Unsafe Message from {get_name_from_number(message_from_id, 'long', rxNode)}")
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:

View File

@@ -9,10 +9,7 @@ This document provides an overview of all modules available in the Mesh-Bot proj
- [Networking](#networking)
- [Games](#games)
- [BBS (Bulletin Board System)](#bbs-bulletin-board-system)
- [Checklist](#checklist)
- [Inventory & Point of Sale](#inventory--point-of-sale)
- [Location & Weather](#location--weather)
- [Map Command](#map-command)
- [EAS & Emergency Alerts](#eas--emergency-alerts)
- [File Monitoring & News](#file-monitoring--news)
- [Radio Monitoring](#radio-monitoring)
@@ -20,8 +17,10 @@ This document provides an overview of all modules available in the Mesh-Bot proj
- [Ollama LLM/AI](#ollama-llmai)
- [Wikipedia Search](#wikipedia-search)
- [DX Spotter Module](#dx-spotter-module)
- [Mesh Bot Scheduler User Guide](#mesh-bot-scheduler-user-guide)
- [Other Utilities](#other-utilities)
- [Checklist](#checklist)
- [Inventory & Point of Sale](#inventory--point-of-sale)
- [Echo Command](#echo-command)
- [Messaging Settings](#messaging-settings)
- [Troubleshooting](#troubleshooting)
- [Configuration Guide](#configuration-guide)
@@ -296,6 +295,7 @@ The system uses SQLite with four tables:
| `howfar` | Distance traveled since last check |
| `howtall` | Calculate height using sun angle |
| `whereami` | Show current location |
| `map` | Location data/map.csv |
Configure in `[location]` section of `config.ini`.
@@ -341,7 +341,6 @@ The `map` command allows you to log your current GPS location with a custom desc
|--------------|-----------------------------------------------|
| `ea`/`ealert`| FEMA iPAWS/EAS alerts (USA/DE) |
Enable in `[eas]` section of `config.ini`.
---
@@ -363,10 +362,6 @@ The Radio Monitoring module provides several ways to integrate amateur radio sof
### Hamlib Integration
| Command | Description |
|--------------|-----------------------------------------------|
| `radio` | Monitor radio SNR via Hamlib |
Monitors signal strength (S-meter) from a connected radio via Hamlib's `rigctld` daemon. When the signal exceeds a configured threshold, it broadcasts an alert to the mesh network with frequency and signal strength information.
### WSJT-X Integration
@@ -457,9 +452,6 @@ Enable and configure VOX features in the `[vox]` section of `config.ini`.
| Command | Description |
|--------------|-----------------------------------------------|
| `askai` | Ask Ollama LLM AI |
| `ask:` | Ask Ollama LLM AI (raw) |
Configure in `[ollama]` section of `config.ini`.
More at [LLM Readme](llm.md)
@@ -471,7 +463,7 @@ More at [LLM Readme](llm.md)
|--------------|-----------------------------------------------|
| `wiki` | Search Wikipedia or local Kiwix server |
Configure in `[wikipedia]` section of `config.ini`.
Configure in `[general]` section of `config.ini`.
---
@@ -743,6 +735,73 @@ You can use any of these options to schedule messages on specific days:
- `history` — Command history
- `cmd`/`cmd?` — Show help message (the bot avoids the use of saying or using help)
| Command | Description | ✅ Works Off-Grid |
|--------------|-------------|------------------|
| `echo` | Echo string back. Admins can use `echo <message> c=<channel> d=<device>` to send to any channel/device. | ✅ |
---
### Echo Command
The `echo` command returns your message back to you.
**Admins** can use an extended syntax to send a message to any channel and device.
#### Usage
- **Basic Echo (all users):**
```
echo Hello World
```
Response:
```
Hello World
```
- **Admin Extended Syntax:**
```
echo <message> c=<channel> d=<device>
```
Example:
```
echo Hello world c=1 d=2
```
This will send "Hello world" to channel 1, device 2.
#### Special Keyword Substitution
- In admin echo, if you include the word `motd` or `MOTD` (case-insensitive), it will be replaced with the current Message of the Day.
- If you include the word `welcome!` (case-insensitive), it will be replaced with the current Welcome Message as set in your configuration.
- Example:
```
echo Today's message is motd c=1 d=2
```
If the MOTD is "Potatos Are Cool!", the message sent will be:
```
Today's message is Potatos Are Cool!
```
#### Notes
- Only admins can use the `c=<channel>` and `d=<device>` override.
- If you omit `c=<channel>` and `d=<device>`, the message is echoed back to your current channel/device.
- MOTD substitution works for any standalone `motd` or `MOTD` in the message.
#### Help
- Send `echo?` for usage instructions.
- Admins will see this help message:
```
Admin usage: echo <message> c=<channel> d=<device>
Example: echo Hello world c=1 d=2
```
#### Notes
- Only admins can use the `c=<channel>` and `d=<device>` override.
- If you omit `c=<channel>` and `d=<device>`, the message is echoed back to your current channel/device.
---
## Configuration
@@ -1194,6 +1253,4 @@ enabled = True # QRZ Hello to new nodes
qrz_hello_string = "send CMD or DM me for more info." # will be sent to all heard nodes once
training = True # Training mode will not send the hello message to new nodes, use this to build up database
```
Happy meshing!

View File

@@ -1004,39 +1004,42 @@ def distance(lat=0,lon=0,nodeID=0, reset=False):
return msg
def get_openskynetwork(lat=0, lon=0):
# get the latest aircraft data from OpenSky Network in the area
def get_openskynetwork(lat=0, lon=0, altitude=0, node_altitude=0, altitude_window=1000):
"""
Returns the aircraft dict from OpenSky Network closest in altitude (within altitude_window meters)
to the given node_altitude. If no aircraft found, returns my_settings.NO_ALERTS.
"""
if lat == 0 and lon == 0:
return my_settings.NO_ALERTS
# setup a bounding box of 50km around the lat/lon
box_size = 0.45 # approx 50km
# return limits for aircraft search
search_limit = 3
return False
box_size = 0.45 # approx 50km
lamin = lat - box_size
lamax = lat + box_size
lomin = lon - box_size
lomax = lon + box_size
# fetch the aircraft data from OpenSky Network
opensky_url = f"https://opensky-network.org/api/states/all?lamin={lamin}&lomin={lomin}&lamax={lamax}&lomax={lomax}"
opensky_url = (
f"https://opensky-network.org/api/states/all?lamin={lamin}&lomin={lomin}"
f"&lamax={lamax}&lomax={lomax}"
)
try:
aircraft_data = requests.get(opensky_url, timeout=my_settings.urlTimeoutSeconds)
if not aircraft_data.ok:
logger.warning("Location:Error fetching aircraft data from OpenSky Network")
return my_settings.ERROR_FETCHING_DATA
return False
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching aircraft data from OpenSky Network")
return my_settings.ERROR_FETCHING_DATA
return False
aircraft_json = aircraft_data.json()
if 'states' not in aircraft_json or not aircraft_json['states']:
return my_settings.NO_ALERTS
return False
aircraft_list = aircraft_json['states']
aircraft_report = ""
logger.debug(f"Location: OpenSky Network: Found {len(aircraft_list)} possible aircraft in area")
closest = None
min_diff = float('inf')
for aircraft in aircraft_list:
if len(aircraft_report.split("\n")) >= search_limit:
break
# extract values from JSON
try:
callsign = aircraft[1].strip() if aircraft[1] else "N/A"
origin_country = aircraft[2]
@@ -1044,20 +1047,37 @@ def get_openskynetwork(lat=0, lon=0):
true_track = aircraft[10]
vertical_rate = aircraft[11]
sensors = aircraft[12]
baro_altitude = aircraft[7]
geo_altitude = aircraft[13]
squawk = aircraft[14] if len(aircraft) > 14 else "N/A"
except Exception as e:
logger.debug("Location:Error extracting aircraft data from OpenSky Network")
continue
# format the aircraft data
aircraft_report += f"{callsign} Alt:{int(geo_altitude) if geo_altitude else 'N/A'}m Vel:{int(velocity) if velocity else 'N/A'}m/s Heading:{int(true_track) if true_track else 'N/A'}°\n"
# remove last newline
if aircraft_report.endswith("\n"):
aircraft_report = aircraft_report[:-1]
aircraft_report = abbreviate_noaa(aircraft_report)
return aircraft_report if aircraft_report else my_settings.NO_ALERTS
# Prefer geo_altitude, fallback to baro_altitude
plane_alt = geo_altitude if geo_altitude is not None else baro_altitude
if plane_alt is None or node_altitude == 0:
continue
diff = abs(plane_alt - node_altitude)
if diff <= altitude_window and diff < min_diff:
min_diff = diff
closest = {
"callsign": callsign,
"origin_country": origin_country,
"velocity": velocity,
"true_track": true_track,
"vertical_rate": vertical_rate,
"sensors": sensors,
"altitude": baro_altitude,
"geo_altitude": geo_altitude,
"squawk": squawk,
}
if closest:
return closest
else:
return False
def log_locationData_toMap(userID, location, message):
"""

View File

@@ -35,6 +35,7 @@ voxMsgQueue = [] # queue for VOX detected messages
tts_read_queue = [] # queue for TTS messages
wsjtxMsgQueue = [] # queue for WSJT-X detected messages
js8callMsgQueue = [] # queue for JS8Call detected messages
autoBanlist = [] # list of nodes to autoban for repeated offenses
# Game trackers
surveyTracker = [] # Survey game tracker
tictactoeTracker = [] # TicTacToe game tracker
@@ -81,7 +82,7 @@ if 'sentry' not in config:
config.write(open(config_file, 'w'))
if 'location' not in config:
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'fuzzConfigLocation': 'True',}
config.write(open(config_file, 'w'))
if 'bbs' not in config:
@@ -325,7 +326,6 @@ try:
if eAlertBroadcastEnabled or ipawsAlertEnabled:
eAlertBroadcastEnabled = True
ipawsAlertEnabled = True
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
volcanoAlertBroadcastEnabled = config['location'].getboolean('volcanoAlertBroadcastEnabled', False) # default False
enableGBalerts = config['location'].getboolean('enableGBalerts', False) # default False
@@ -351,7 +351,6 @@ try:
ipawsAlertEnabled or
wxAlertBroadcastEnabled or
volcanoAlertBroadcastEnabled or
wxAlertsEnabled or
eAlertBroadcastEnabled
)
@@ -494,6 +493,9 @@ try:
noisyNodeLogging = config['messagingSettings'].getboolean('noisyNodeLogging', False) # default False
logMetaStats = config['messagingSettings'].getboolean('logMetaStats', True) # default True
noisyTelemetryLimit = config['messagingSettings'].getint('noisyTelemetryLimit', 5) # default 5 packets
autoBanEnabled = config['messagingSettings'].getboolean('autoBanEnabled', False) # default False
autoBanThreshold = config['messagingSettings'].getint('autoBanThreshold', 5) # default 5 offenses
autoBanTimeframe = config['messagingSettings'].getint('autoBanTimeframe', 3600) # default 1 hour in seconds
except Exception as e:
print(f"System: Error reading config file: {e}")
print("System: Check the config.ini against config.template file for missing sections or values.")

View File

@@ -951,21 +951,94 @@ def messageTrap(msg):
return True
return False
def stringSafeCheck(s):
def stringSafeCheck(s, fromID=0):
# Check if a string is safe to use, no control characters or non-printable characters
soFarSoGood = True
if not all(c.isprintable() or c.isspace() for c in s):
return False
ban_hammer(fromID, reason="Non-printable character in message")
return False # non-printable characters found
if any(ord(c) < 32 and c not in '\n\r\t' for c in s):
return False
ban_hammer(fromID, reason="Control character in message")
return False # control characters found
if any(c in s for c in ['\x0b', '\x0c', '\x1b']):
return False
return False # vertical tab, form feed, escape characters found
if len(s) > 1000:
return False
injection_chars = [';', '|', '../']
if any(char in s for char in injection_chars):
# Check for single-character injections
single_injection_chars = [';', '|', '}', '>', ')']
if any(c in s for c in single_injection_chars):
return False # injection character found
# Check for multi-character patterns
multi_injection_patterns = ['../', '||']
if any(pattern in s for pattern in multi_injection_patterns):
return False
return soFarSoGood
return True
def ban_hammer(node_id, rxInterface=None, channel=None, reason=""):
"""
Auto-ban nodes that exceed the message threshold within the timeframe.
Returns True if the node is (or becomes) banned, False otherwise.
"""
global autoBanlist, seenNodes, bbs_ban_list
current_time = time.time()
node_id_str = str(node_id)
if isNodeAdmin(node_id_str):
return False # Do not ban admin nodes
# Check if the node is already banned
if node_id_str in bbs_ban_list or node_id_str in autoBanlist:
return True # Node is already banned
# if no reason provided, dont ban just run that last check
if reason == "":
return False
# Find or create the seenNodes entry (patched for missing 'node_id')
node_entry = next((entry for entry in seenNodes if entry.get('node_id') == node_id_str), None)
if node_entry:
# Update interface and channel if provided
if rxInterface is not None:
node_entry['rxInterface'] = rxInterface
if channel is not None:
node_entry['channel'] = channel
# Check if the timeframe has expired
if (current_time - node_entry['lastSeen']) > autoBanTimeframe:
node_entry['auto_ban_count'] = 1
node_entry['lastSeen'] = current_time
else:
node_entry['auto_ban_count'] += 1
node_entry['lastSeen'] = current_time
else:
# node not found, create a new entry
entry = {
'node_id': node_id_str,
'first_seen': current_time,
'lastSeen': current_time,
'auto_ban_count': 3, # start at 3 to trigger ban faster
'rxInterface': rxInterface,
'channel': channel,
'welcome': False
}
seenNodes.append(entry)
node_entry = entry
# Check if the node has exceeded the ban threshold
if node_entry['auto_ban_count'] < autoBanThreshold:
logger.debug(f"System: Node {node_id_str} auto-ban count: {node_entry['auto_ban_count']}")
return False # No ban applied
# If the node has exceeded the ban threshold within the time window
autoBanlist.append(node_id_str)
logger.info(f"System: Node {node_id_str} exceeded auto-ban threshold with {node_entry['auto_ban_count']} messages")
if autoBanEnabled:
logger.warning(f"System: Auto-banned node {node_id_str} Reason: {reason}")
if node_id_str not in bbs_ban_list:
bbs_ban_list.append(node_id_str)
save_bbsBanList()
return True # Node is now banned
return False # No ban applied
def save_bbsBanList():
# save the bbs_ban_list to file
@@ -983,7 +1056,7 @@ def load_bbsBanList():
try:
with open('data/bbs_ban_list.txt', 'r') as f:
loaded_list = [line.strip() for line in f if line.strip()]
logger.debug("System: BBS ban list loaded from file")
logger.debug(f"System: BBS ban list now has {len(loaded_list)} entries loaded from file")
except FileNotFoundError:
config_val = config['bbs'].get('bbs_ban_list', '')
if config_val:
@@ -1003,8 +1076,6 @@ def isNodeAdmin(nodeID):
for admin in bbs_admin_list:
if str(nodeID) == admin:
return True
else:
return True
return False
def isNodeBanned(nodeID):
@@ -1015,6 +1086,7 @@ def isNodeBanned(nodeID):
return False
def handle_bbsban(message, message_from_id, isDM):
global bbs_ban_list
msg = ""
if not isDM:
return "🤖only available in a Direct Message📵"
@@ -1518,25 +1590,26 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
if current_time - last_alert_time < 1800:
return False # less than 30 minutes since last alert
positionMetadata[nodeID]['lastHighFlyAlert'] = current_time
if highfly_check_openskynetwork:
# check get_openskynetwork to see if the node is an aircraft
if 'latitude' in position_data and 'longitude' in position_data:
flight_info = get_openskynetwork(position_data.get('latitude', 0), position_data.get('longitude', 0))
# Only show plane if within altitude
if (
flight_info
and NO_ALERTS not in flight_info
and ERROR_FETCHING_DATA not in flight_info
and isinstance(flight_info, dict)
and 'altitude' in flight_info
):
plane_alt = flight_info['altitude']
node_alt = position_data.get('altitude', 0)
if abs(node_alt - plane_alt) <= 1000: # within 1000 meters
msg += f"\nDetected near:\n{flight_info}"
send_message(msg, highfly_channel, 0, highfly_interface)
try:
if highfly_check_openskynetwork:
if 'latitude' in position_data and 'longitude' in position_data and 'altitude' in position_data:
flight_info = get_openskynetwork(
position_data.get('latitude', 0),
position_data.get('longitude', 0),
node_altitude=position_data.get('altitude', 0)
)
if flight_info and isinstance(flight_info, dict):
msg += (
f"\nDetected near:\n"
f"{flight_info.get('callsign', 'N/A')} "
f"Alt:{int(flight_info.get('geo_altitude', 0)) if flight_info.get('geo_altitude') else 'N/A'}m "
f"Vel:{int(flight_info.get('velocity', 0)) if flight_info.get('velocity') else 'N/A'}m/s "
f"Heading:{int(flight_info.get('true_track', 0)) if flight_info.get('true_track') else 'N/A'}°\n"
f"From:{flight_info.get('origin_country', 'N/A')}"
)
send_message(msg, highfly_channel, 0, highfly_interface)
except Exception as e:
logger.debug(f"System: Highfly: error: {e}")
# Keep the positionMetadata dictionary at a maximum size
if len(positionMetadata) > MAX_SEEN_NODES:
# Remove the oldest entry

View File

@@ -77,6 +77,13 @@ class TestBot(unittest.TestCase):
self.assertTrue(result)
self.assertIsInstance(result1, str)
def test_initialize_inventory_database(self):
from inventory import initialize_inventory_database, process_inventory_command
result = initialize_inventory_database()
result1 = process_inventory_command(0, 'inventory', name="none")
self.assertTrue(result)
self.assertIsInstance(result1, str)
def test_init_news_sources(self):
from filemon import initNewsSources
result = initNewsSources()
@@ -87,11 +94,6 @@ class TestBot(unittest.TestCase):
alerts = get_nina_alerts()
self.assertIsInstance(alerts, str)
def test_llmTool_get_google(self):
from llm import llmTool_get_google
result = llmTool_get_google("What is 2+2?", 1)
self.assertIsInstance(result, list)
def test_send_ollama_query(self):
from llm import send_ollama_query
response = send_ollama_query("Hello, Ollama!")
@@ -150,10 +152,13 @@ class TestBot(unittest.TestCase):
result = initalize_qrz_database()
self.assertTrue(result)
def test_get_hamlib(self):
from radio import get_hamlib
frequency = get_hamlib('f')
self.assertIsInstance(frequency, str)
def test_import_radio_module(self):
try:
import radio
#frequency = get_hamlib('f')
#self.assertIsInstance(frequency, str)
except Exception as e:
self.fail(f"Importing radio module failed: {e}")
def test_get_rss_feed(self):
from rss import get_rss_feed

View File

@@ -65,7 +65,11 @@ def handle_cmd(message, message_from_id, deviceID):
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
global multiPing
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag"
pingHelp = "🤖Ping Command Help:\n" \
"🏓 Send 'ping' or 'ack' or 'test' to get a response.\n" \
"🏓 Send 'ping <number>' to get multiple pings in DM"
"🏓 ping @USERID to send a Joke from the bot"
return pingHelp
msg = ""
type = ''
@@ -303,10 +307,21 @@ def onReceive(packet, interface):
# set the message_from_id
message_from_id = packet['from']
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
# if message_from_id is not in the seenNodes list add it
if not any(node.get('nodeID') == message_from_id for node in seenNodes):
seenNodes.append({'nodeID': message_from_id, 'rxInterface': rxNode, 'channel': channel_number, 'welcome': False, 'first_seen': time.time(), 'lastSeen': time.time()})
else:
# update lastSeen time
for node in seenNodes:
if node.get('nodeID') == message_from_id:
node['lastSeen'] = time.time()
break
# CHECK with ban_hammer() if the node is banned
if str(message_from_id) in my_settings.bbs_ban_list or str(message_from_id) in my_settings.autoBanlist:
logger.warning(f"System: Banned Node {message_from_id} tried to send a message. Ignored. Try adding to node firmware-blocklist")
return
# handle TEXT_MESSAGE_APP
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
@@ -379,7 +394,7 @@ def onReceive(packet, interface):
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism} Hostname:{rxNodeHostName}")
# check with stringSafeChecker if the message is safe
if stringSafeCheck(message_string) is False:
if stringSafeCheck(message_string, message_from_id) is False:
logger.warning(f"System: Possibly Unsafe Message from {get_name_from_number(message_from_id, 'long', rxNode)}")
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:
@@ -574,6 +589,10 @@ def handle_boot(mesh=True):
if my_settings.useDMForResponse:
logger.debug("System: Respond by DM only")
if my_settings.autoBanEnabled:
logger.debug(f"System: Auto-Ban Enabled for {my_settings.autoBanThreshold} messages in {my_settings.autoBanTimeframe} seconds")
load_bbsBanList()
if my_settings.log_messages_to_file:
logger.debug("System: Logging Messages to disk")
if my_settings.syslog_to_file: