mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c6fcf10ef | ||
|
|
647ae92649 | ||
|
|
254eef4be9 | ||
|
|
bd0a94e2a1 | ||
|
|
2d8256d9f7 | ||
|
|
1f9b81865e | ||
|
|
17221cf37f | ||
|
|
47dd75bfb3 | ||
|
|
d4773705ce | ||
|
|
4f46e659d9 | ||
|
|
404f84f39c | ||
|
|
c07ec534a7 | ||
|
|
4d88aed0d8 | ||
|
|
b1946608f4 |
@@ -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
|
||||
95
mesh_bot.py
95
mesh_bot.py
@@ -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:
|
||||
|
||||
@@ -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!
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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"\n✈️Detected 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"\n✈️Detected 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
|
||||
|
||||
@@ -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
|
||||
|
||||
29
pong_bot.py
29
pong_bot.py
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user