Add outgoing message region tagging. Closes #35.

This commit is contained in:
Jack Kingsman
2026-03-04 15:42:21 -08:00
parent c2931a266e
commit 145609faf9
15 changed files with 339 additions and 27 deletions
+1
View File
@@ -244,6 +244,7 @@ Main tables:
- `mqtt_broker_host`, `mqtt_broker_port`, `mqtt_username`, `mqtt_password`
- `mqtt_use_tls`, `mqtt_tls_insecure`, `mqtt_topic_prefix`, `mqtt_publish_messages`, `mqtt_publish_raw_packets`
- `community_mqtt_enabled`, `community_mqtt_iata`, `community_mqtt_broker_host`, `community_mqtt_broker_port`, `community_mqtt_email`
- `flood_scope`
## Security Posture (intentional)
+25
View File
@@ -268,6 +268,13 @@ async def run_migrations(conn: aiosqlite.Connection) -> int:
await set_version(conn, 33)
applied += 1
# Migration 34: Add flood_scope column to app_settings
if version < 34:
logger.info("Applying migration 34: add flood_scope column to app_settings")
await _migrate_034_add_flood_scope(conn)
await set_version(conn, 34)
applied += 1
if applied > 0:
logger.info(
"Applied %d migration(s), schema now at version %d", applied, await get_version(conn)
@@ -1951,3 +1958,21 @@ async def _migrate_033_seed_remoteterm_channel(conn: aiosqlite.Connection) -> No
await conn.commit()
except Exception:
logger.debug("Skipping #remoteterm seed (channels table not ready)")
async def _migrate_034_add_flood_scope(conn: aiosqlite.Connection) -> None:
"""Add flood_scope column to app_settings for outbound region tagging.
Empty string means disabled (no scope set, messages sent unscoped).
"""
try:
await conn.execute("ALTER TABLE app_settings ADD COLUMN flood_scope TEXT DEFAULT ''")
await conn.commit()
except Exception as e:
error_msg = str(e).lower()
if "duplicate column" in error_msg:
logger.debug("flood_scope column already exists, skipping")
elif "no such table" in error_msg:
logger.debug("app_settings table not ready, skipping flood_scope migration")
else:
raise
+4
View File
@@ -518,6 +518,10 @@ class AppSettings(BaseModel):
default="",
description="Email address for node claiming on the community aggregator (optional)",
)
flood_scope: str = Field(
default="",
description="Outbound flood scope / region name (empty = disabled, no tagging)",
)
class BusyChannel(BaseModel):
+8
View File
@@ -258,6 +258,14 @@ class RadioManager:
# Sync radio clock with system time
await sync_radio_time(mc)
# Apply flood scope from settings
from app.repository import AppSettingsRepository
app_settings = await AppSettingsRepository.get()
scope = app_settings.flood_scope
await mc.commands.set_flood_scope(scope if scope else "")
logger.info("Applied flood_scope=%r", scope or "(disabled)")
# Sync contacts/channels from radio to DB and clear radio
logger.info("Syncing and offloading radio data...")
result = await sync_and_offload_all(mc)
+7 -1
View File
@@ -32,7 +32,7 @@ class AppSettingsRepository:
mqtt_publish_messages, mqtt_publish_raw_packets,
community_mqtt_enabled, community_mqtt_iata,
community_mqtt_broker_host, community_mqtt_broker_port,
community_mqtt_email
community_mqtt_email, flood_scope
FROM app_settings WHERE id = 1
"""
)
@@ -112,6 +112,7 @@ class AppSettingsRepository:
or "mqtt-us-v1.letsmesh.net",
community_mqtt_broker_port=row["community_mqtt_broker_port"] or 443,
community_mqtt_email=row["community_mqtt_email"] or "",
flood_scope=row["flood_scope"] or "",
)
@staticmethod
@@ -139,6 +140,7 @@ class AppSettingsRepository:
community_mqtt_broker_host: str | None = None,
community_mqtt_broker_port: int | None = None,
community_mqtt_email: str | None = None,
flood_scope: str | None = None,
) -> AppSettings:
"""Update app settings. Only provided fields are updated."""
updates = []
@@ -238,6 +240,10 @@ class AppSettingsRepository:
updates.append("community_mqtt_email = ?")
params.append(community_mqtt_email)
if flood_scope is not None:
updates.append("flood_scope = ?")
params.append(flood_scope)
if updates:
query = f"UPDATE app_settings SET {', '.join(updates)} WHERE id = 1"
await db.conn.execute(query, params)
+24
View File
@@ -121,6 +121,10 @@ class AppSettingsUpdate(BaseModel):
default=None,
description="Email address for node claiming on the community aggregator",
)
flood_scope: str | None = Field(
default=None,
description="Outbound flood scope / region name (empty = disabled)",
)
class FavoriteRequest(BaseModel):
@@ -237,6 +241,13 @@ async def update_settings(update: AppSettingsUpdate) -> AppSettings:
kwargs["community_mqtt_email"] = update.community_mqtt_email
community_mqtt_changed = True
# Flood scope
flood_scope_changed = False
if update.flood_scope is not None:
stripped = update.flood_scope.strip()
kwargs["flood_scope"] = stripped
flood_scope_changed = True
# Require IATA when enabling community MQTT
if kwargs.get("community_mqtt_enabled", False):
# Check the IATA value being set, or fall back to current settings
@@ -265,6 +276,19 @@ async def update_settings(update: AppSettingsUpdate) -> AppSettings:
await community_publisher.restart(result)
# Apply flood scope to radio immediately if changed
if flood_scope_changed:
from app.radio import radio_manager
if radio_manager.is_connected:
try:
scope = result.flood_scope
async with radio_manager.radio_operation("set_flood_scope") as mc:
await mc.commands.set_flood_scope(scope if scope else "")
logger.info("Applied flood_scope=%r to radio", scope or "(disabled)")
except Exception as e:
logger.warning("Failed to apply flood_scope to radio: %s", e)
return result
return await AppSettingsRepository.get()