From 23da88c37df6d807744673d529ed8224aebffb49 Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 18 May 2026 17:19:46 -0700 Subject: [PATCH] refactor(shared): create shared/ package and migrate foundation modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move models.py, db_manager.py, db_migrations.py, and security_utils.py from modules/ to a new shared/ top-level package that can be imported by both the bot and the web viewer without coupling them. Update all imports across ~75 files (commands, service plugins, tests, web viewer, generate_website.py). No logic changes — pure file moves and import path updates. Co-Authored-By: Claude Sonnet 4.6 --- generate_website.py | 2 +- modules/clients/mqtt_weather.py | 2 +- modules/command_manager.py | 4 +-- modules/commands/advert_command.py | 2 +- modules/commands/airplanes_command.py | 2 +- modules/commands/alert_command.py | 2 +- .../commands/alternatives/wx_international.py | 2 +- modules/commands/announcements_command.py | 4 +-- modules/commands/aqi_command.py | 2 +- modules/commands/aurora_command.py | 2 +- modules/commands/base_command.py | 4 +-- modules/commands/catfact_command.py | 2 +- modules/commands/channelpause_command.py | 2 +- modules/commands/channels_command.py | 2 +- modules/commands/cmd_command.py | 2 +- modules/commands/dadjoke_command.py | 2 +- modules/commands/dice_command.py | 2 +- modules/commands/feed_command.py | 4 +-- modules/commands/greeter_command.py | 2 +- modules/commands/hacker_command.py | 2 +- modules/commands/hello_command.py | 2 +- modules/commands/help_command.py | 2 +- modules/commands/hfcond_command.py | 2 +- modules/commands/joke_command.py | 2 +- modules/commands/magic8_command.py | 2 +- modules/commands/moon_command.py | 2 +- modules/commands/multitest_command.py | 2 +- modules/commands/path_command.py | 4 +-- modules/commands/ping_command.py | 2 +- modules/commands/prefix_command.py | 2 +- modules/commands/reload_command.py | 2 +- modules/commands/repeater_command.py | 2 +- modules/commands/roll_command.py | 2 +- modules/commands/satpass_command.py | 2 +- modules/commands/schedule_command.py | 2 +- modules/commands/solar_command.py | 2 +- modules/commands/solarforecast_command.py | 4 +-- modules/commands/sports_command.py | 2 +- modules/commands/stats_command.py | 2 +- modules/commands/status_command.py | 2 +- modules/commands/sun_command.py | 2 +- modules/commands/test_command.py | 2 +- modules/commands/trace_command.py | 2 +- modules/commands/version_command.py | 2 +- modules/commands/webviewer_command.py | 2 +- modules/commands/wx_command.py | 2 +- modules/core.py | 2 +- modules/feed_manager.py | 2 +- modules/maintenance.py | 2 +- modules/message_handler.py | 4 +-- modules/repeater_manager.py | 2 +- modules/scheduler.py | 2 +- .../service_plugins/discord_bridge_service.py | 2 +- modules/web_viewer/app.py | 4 +-- pyproject.toml | 5 +-- shared/__init__.py | 0 {modules => shared}/db_manager.py | 0 {modules => shared}/db_migrations.py | 0 {modules => shared}/models.py | 0 shared/parsers/__init__.py | 0 {modules => shared}/security_utils.py | 0 tests/commands/test_base_command.py | 2 +- tests/conftest.py | 4 +-- tests/integration/test_flood_scope_reply.py | 2 +- tests/test_command_manager.py | 2 +- tests/test_command_prefix.py | 2 +- tests/test_db_manager.py | 2 +- tests/test_db_migrations.py | 2 +- tests/test_feed_manager.py | 2 +- tests/test_feed_manager_extended.py | 2 +- tests/test_message_handler.py | 2 +- tests/test_models.py | 2 +- tests/test_plugin_loader.py | 2 +- tests/test_security_utils.py | 36 +++++++++---------- tests/test_web_viewer.py | 4 +-- tests/unit/test_flood_scope_resolve.py | 2 +- tests/unit/test_path_command_multibyte.py | 2 +- .../test_path_command_utf8_message_limits.py | 2 +- tests/unit/test_response_template.py | 2 +- 79 files changed, 102 insertions(+), 101 deletions(-) create mode 100644 shared/__init__.py rename {modules => shared}/db_manager.py (100%) rename {modules => shared}/db_migrations.py (100%) rename {modules => shared}/models.py (100%) create mode 100644 shared/parsers/__init__.py rename {modules => shared}/security_utils.py (100%) diff --git a/generate_website.py b/generate_website.py index 4b8a201..6a37293 100755 --- a/generate_website.py +++ b/generate_website.py @@ -21,7 +21,7 @@ try: sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from modules.config_validation import strip_optional_quotes - from modules.db_manager import DBManager + from shared.db_manager import DBManager from modules.plugin_loader import PluginLoader from modules.utils import resolve_path except ImportError as e: diff --git a/modules/clients/mqtt_weather.py b/modules/clients/mqtt_weather.py index 4ed0169..2c39bf6 100644 --- a/modules/clients/mqtt_weather.py +++ b/modules/clients/mqtt_weather.py @@ -16,7 +16,7 @@ from configparser import ConfigParser from dataclasses import dataclass from typing import Any -from ..security_utils import sanitize_input +from shared.security_utils import sanitize_input MQTT_WEATHER_PREFIX = "custom.mqtt_weather." diff --git a/modules/command_manager.py b/modules/command_manager.py index 2e90650..bdf7169 100644 --- a/modules/command_manager.py +++ b/modules/command_manager.py @@ -21,9 +21,9 @@ from .config_validation import ( _channel_name_is_public, strip_optional_quotes, ) -from .models import CHANNEL_REGIONAL_FLOOD_SCOPE_BODY_OVERHEAD, MeshMessage +from shared.models import CHANNEL_REGIONAL_FLOOD_SCOPE_BODY_OVERHEAD, MeshMessage from .plugin_loader import PluginLoader -from .security_utils import sanitize_name, validate_safe_path +from shared.security_utils import sanitize_name, validate_safe_path from .utils import check_internet_connectivity_async, decode_escape_sequences, format_keyword_response_with_placeholders diff --git a/modules/commands/advert_command.py b/modules/commands/advert_command.py index 042c8a3..cff5599 100644 --- a/modules/commands/advert_command.py +++ b/modules/commands/advert_command.py @@ -8,7 +8,7 @@ import asyncio import time from typing import Any -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/airplanes_command.py b/modules/commands/airplanes_command.py index cc74d91..5a0f983 100644 --- a/modules/commands/airplanes_command.py +++ b/modules/commands/airplanes_command.py @@ -11,7 +11,7 @@ from typing import Any, Optional import requests -from ..models import MeshMessage +from shared.models import MeshMessage from ..utils import calculate_distance from .base_command import BaseCommand diff --git a/modules/commands/alert_command.py b/modules/commands/alert_command.py index a4a91bf..87f0c2b 100644 --- a/modules/commands/alert_command.py +++ b/modules/commands/alert_command.py @@ -15,7 +15,7 @@ import requests from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from ..models import MeshMessage +from shared.models import MeshMessage from ..utils import calculate_distance, geocode_city_sync, geocode_zipcode_sync, rate_limited_nominatim_reverse_sync from .base_command import BaseCommand diff --git a/modules/commands/alternatives/wx_international.py b/modules/commands/alternatives/wx_international.py index 45576cf..5d4897c 100644 --- a/modules/commands/alternatives/wx_international.py +++ b/modules/commands/alternatives/wx_international.py @@ -10,7 +10,7 @@ from typing import Any, Optional, Union import requests -from ...models import MeshMessage +from shared.models import MeshMessage from ...utils import ( format_temperature_high_low, geocode_city_sync, diff --git a/modules/commands/announcements_command.py b/modules/commands/announcements_command.py index 7660ba3..a88ec8e 100644 --- a/modules/commands/announcements_command.py +++ b/modules/commands/announcements_command.py @@ -6,8 +6,8 @@ Allows authorized users to send announcements to channels via DM import time -from ..models import MeshMessage -from ..security_utils import validate_pubkey_format +from shared.models import MeshMessage +from shared.security_utils import validate_pubkey_format from .base_command import BaseCommand diff --git a/modules/commands/aqi_command.py b/modules/commands/aqi_command.py index 2d6fa72..36e18f8 100644 --- a/modules/commands/aqi_command.py +++ b/modules/commands/aqi_command.py @@ -10,7 +10,7 @@ import openmeteo_requests import requests_cache from retry_requests import retry -from ..models import MeshMessage +from shared.models import MeshMessage from ..utils import ( abbreviate_location, geocode_city_sync, diff --git a/modules/commands/aurora_command.py b/modules/commands/aurora_command.py index 6b9e430..35c71bd 100644 --- a/modules/commands/aurora_command.py +++ b/modules/commands/aurora_command.py @@ -9,7 +9,7 @@ from datetime import datetime, timezone from typing import Optional from ..clients.noaa_aurora_client import NOAAAuroraClient -from ..models import MeshMessage +from shared.models import MeshMessage from ..utils import geocode_city_sync, geocode_zipcode_sync, get_config_timezone from .base_command import BaseCommand diff --git a/modules/commands/base_command.py b/modules/commands/base_command.py index 0ab127b..462ee60 100644 --- a/modules/commands/base_command.py +++ b/modules/commands/base_command.py @@ -9,8 +9,8 @@ from abc import ABC, abstractmethod from datetime import datetime from typing import Any, Optional -from ..models import CHANNEL_REGIONAL_FLOOD_SCOPE_BODY_OVERHEAD, MeshMessage -from ..security_utils import validate_pubkey_format +from shared.models import CHANNEL_REGIONAL_FLOOD_SCOPE_BODY_OVERHEAD, MeshMessage +from shared.security_utils import validate_pubkey_format from ..utils import format_elapsed_display, get_config_timezone diff --git a/modules/commands/catfact_command.py b/modules/commands/catfact_command.py index d092c63..64d48f5 100644 --- a/modules/commands/catfact_command.py +++ b/modules/commands/catfact_command.py @@ -6,7 +6,7 @@ Provides random cat facts as a hidden easter egg command import random -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/channelpause_command.py b/modules/commands/channelpause_command.py index f0b4929..583a46b 100644 --- a/modules/commands/channelpause_command.py +++ b/modules/commands/channelpause_command.py @@ -4,7 +4,7 @@ Channel pause command DM-only admin: pause or resume bot responses on public channels (in-memory only). """ -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/channels_command.py b/modules/commands/channels_command.py index 56474ae..298a13c 100644 --- a/modules/commands/channels_command.py +++ b/modules/commands/channels_command.py @@ -8,7 +8,7 @@ import asyncio import re from typing import Optional -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/cmd_command.py b/modules/commands/cmd_command.py index b45afe5..903970b 100644 --- a/modules/commands/cmd_command.py +++ b/modules/commands/cmd_command.py @@ -6,7 +6,7 @@ Lists available commands in a compact, comma-separated format for LoRa from typing import Any, Optional -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/dadjoke_command.py b/modules/commands/dadjoke_command.py index effa6c1..a152980 100644 --- a/modules/commands/dadjoke_command.py +++ b/modules/commands/dadjoke_command.py @@ -10,7 +10,7 @@ from typing import Any, Optional import aiohttp -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand logger = logging.getLogger("MeshCoreBot") diff --git a/modules/commands/dice_command.py b/modules/commands/dice_command.py index 37e6fa9..e27c7e3 100644 --- a/modules/commands/dice_command.py +++ b/modules/commands/dice_command.py @@ -6,7 +6,7 @@ Handles dice rolling for D&D and other tabletop games import random -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/feed_command.py b/modules/commands/feed_command.py index d20326e..685a62b 100644 --- a/modules/commands/feed_command.py +++ b/modules/commands/feed_command.py @@ -7,8 +7,8 @@ Handles RSS and API feed subscription management import json from typing import Optional -from ..models import MeshMessage -from ..security_utils import sanitize_input, sanitize_name, validate_external_url +from shared.models import MeshMessage +from shared.security_utils import sanitize_input, sanitize_name, validate_external_url from .base_command import BaseCommand diff --git a/modules/commands/greeter_command.py b/modules/commands/greeter_command.py index 0dffd28..931833d 100644 --- a/modules/commands/greeter_command.py +++ b/modules/commands/greeter_command.py @@ -10,7 +10,7 @@ import time from datetime import datetime, timedelta from typing import Any, Optional -from ..models import MeshMessage +from shared.models import MeshMessage from ..utils import decode_escape_sequences from .base_command import BaseCommand diff --git a/modules/commands/hacker_command.py b/modules/commands/hacker_command.py index 416c6b4..735e3f4 100644 --- a/modules/commands/hacker_command.py +++ b/modules/commands/hacker_command.py @@ -7,7 +7,7 @@ Responds to Linux commands with hilarious supervillain mainframe error messages import random from typing import Any -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/hello_command.py b/modules/commands/hello_command.py index 9e2912e..8a64149 100644 --- a/modules/commands/hello_command.py +++ b/modules/commands/hello_command.py @@ -9,7 +9,7 @@ import random import re from typing import Any -from ..models import MeshMessage +from shared.models import MeshMessage from ..utils import get_config_timezone from .base_command import BaseCommand diff --git a/modules/commands/help_command.py b/modules/commands/help_command.py index f0c9904..552205a 100644 --- a/modules/commands/help_command.py +++ b/modules/commands/help_command.py @@ -7,7 +7,7 @@ Provides help information for commands and general usage from collections import defaultdict from typing import Any, Optional -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/hfcond_command.py b/modules/commands/hfcond_command.py index c24912b..19f0a80 100644 --- a/modules/commands/hfcond_command.py +++ b/modules/commands/hfcond_command.py @@ -3,7 +3,7 @@ HF Conditions Command - Provides HF band conditions for ham radio """ -from ..models import MeshMessage +from shared.models import MeshMessage from ..solar_conditions import hf_band_conditions from .base_command import BaseCommand diff --git a/modules/commands/joke_command.py b/modules/commands/joke_command.py index 2e0117c..adfcedb 100644 --- a/modules/commands/joke_command.py +++ b/modules/commands/joke_command.py @@ -9,7 +9,7 @@ from typing import Any, Optional import aiohttp -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/magic8_command.py b/modules/commands/magic8_command.py index f2af7e1..996cd4d 100644 --- a/modules/commands/magic8_command.py +++ b/modules/commands/magic8_command.py @@ -6,7 +6,7 @@ Handles the 'magic8' keyword response import random from typing import Optional -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand magic8_responses = ["It is certain.","It is decidedly so.","Without a doubt.","Yes definitely.","You may rely on it.","As I see it, yes.","Most likely.","Outlook good.","Yes.","Signs point to yes.","Reply hazy, try again.","Ask again later.","Better not tell you now.","Cannot predict now.","Concentrate and ask again.","Don't count on it.","My reply is no.","My sources say no.","Outlook not so good.","Very doubtful."] diff --git a/modules/commands/moon_command.py b/modules/commands/moon_command.py index 67ef3c8..adf0bb0 100644 --- a/modules/commands/moon_command.py +++ b/modules/commands/moon_command.py @@ -3,7 +3,7 @@ Moon Command - Provides moon phase and position information """ -from ..models import MeshMessage +from shared.models import MeshMessage from ..solar_conditions import get_moon from .base_command import BaseCommand diff --git a/modules/commands/multitest_command.py b/modules/commands/multitest_command.py index 3bb4835..f283cce 100644 --- a/modules/commands/multitest_command.py +++ b/modules/commands/multitest_command.py @@ -13,7 +13,7 @@ from typing import Literal, Optional CondensePathsMode = Literal["off", "flat", "nested"] -from ..models import MeshMessage +from shared.models import MeshMessage from ..utils import calculate_packet_hash, parse_path_string from .base_command import BaseCommand diff --git a/modules/commands/path_command.py b/modules/commands/path_command.py index e76fb1e..a35da45 100644 --- a/modules/commands/path_command.py +++ b/modules/commands/path_command.py @@ -10,8 +10,8 @@ import re import time from typing import Any, Callable, Optional -from ..models import MeshMessage -from ..security_utils import sanitize_name +from shared.models import MeshMessage +from shared.security_utils import sanitize_name from ..utils import bytes_per_hop_from_routing_and_nodes, calculate_distance, parse_path_string from .base_command import BaseCommand diff --git a/modules/commands/ping_command.py b/modules/commands/ping_command.py index b68bfb7..570387f 100644 --- a/modules/commands/ping_command.py +++ b/modules/commands/ping_command.py @@ -6,7 +6,7 @@ Handles the 'ping' keyword response from typing import Optional -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/prefix_command.py b/modules/commands/prefix_command.py index ee52bd5..a25e785 100644 --- a/modules/commands/prefix_command.py +++ b/modules/commands/prefix_command.py @@ -14,7 +14,7 @@ from typing import Any, Optional import aiohttp -from ..models import MeshMessage +from shared.models import MeshMessage from ..utils import abbreviate_location, calculate_distance, format_location_for_display, geocode_city, geocode_zipcode from .base_command import BaseCommand diff --git a/modules/commands/reload_command.py b/modules/commands/reload_command.py index 6f0e851..47b336f 100644 --- a/modules/commands/reload_command.py +++ b/modules/commands/reload_command.py @@ -4,7 +4,7 @@ Reload Command Allows admin users to reload the bot configuration without restarting """ -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/repeater_command.py b/modules/commands/repeater_command.py index f245107..3dd72a7 100644 --- a/modules/commands/repeater_command.py +++ b/modules/commands/repeater_command.py @@ -6,7 +6,7 @@ Provides commands to manage repeater contacts and purging operations import asyncio -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/roll_command.py b/modules/commands/roll_command.py index 2032094..fe775af 100644 --- a/modules/commands/roll_command.py +++ b/modules/commands/roll_command.py @@ -7,7 +7,7 @@ Handles random number generation between 1 and X (default 100) import random from typing import Optional -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/satpass_command.py b/modules/commands/satpass_command.py index 197913d..a31c640 100644 --- a/modules/commands/satpass_command.py +++ b/modules/commands/satpass_command.py @@ -3,7 +3,7 @@ Satellite Pass Command - Provides satellite pass information """ -from ..models import MeshMessage +from shared.models import MeshMessage from ..solar_conditions import get_next_satellite_pass from .base_command import BaseCommand diff --git a/modules/commands/schedule_command.py b/modules/commands/schedule_command.py index b45b95f..a5ae2ff 100644 --- a/modules/commands/schedule_command.py +++ b/modules/commands/schedule_command.py @@ -6,7 +6,7 @@ Lists upcoming scheduled messages and interval advertising settings. from typing import Any, Optional -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/solar_command.py b/modules/commands/solar_command.py index 7ffdf88..1ace825 100644 --- a/modules/commands/solar_command.py +++ b/modules/commands/solar_command.py @@ -3,7 +3,7 @@ Solar Command - Provides solar conditions and HF band information """ -from ..models import MeshMessage +from shared.models import MeshMessage from ..solar_conditions import solar_conditions from .base_command import BaseCommand diff --git a/modules/commands/solarforecast_command.py b/modules/commands/solarforecast_command.py index dce27e2..ff2d8b2 100644 --- a/modules/commands/solarforecast_command.py +++ b/modules/commands/solarforecast_command.py @@ -12,8 +12,8 @@ from typing import Optional import requests -from ..models import MeshMessage -from ..security_utils import sanitize_name +from shared.models import MeshMessage +from shared.security_utils import sanitize_name from ..utils import ( abbreviate_location, geocode_city, diff --git a/modules/commands/sports_command.py b/modules/commands/sports_command.py index 037c478..385128e 100644 --- a/modules/commands/sports_command.py +++ b/modules/commands/sports_command.py @@ -25,7 +25,7 @@ from typing import TYPE_CHECKING, Optional from ..clients.espn_client import ESPNClient from ..clients.sports_mappings import LEAGUE_MAPPINGS, SPORT_EMOJIS, TEAM_MAPPINGS from ..clients.thesportsdb_client import TheSportsDBClient -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand if TYPE_CHECKING: diff --git a/modules/commands/stats_command.py b/modules/commands/stats_command.py index 3db176f..753cabf 100644 --- a/modules/commands/stats_command.py +++ b/modules/commands/stats_command.py @@ -7,7 +7,7 @@ Provides comprehensive statistics about bot usage, messages, and activity import time from typing import Any, Optional -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/status_command.py b/modules/commands/status_command.py index 8c2fc8b..6584c8e 100644 --- a/modules/commands/status_command.py +++ b/modules/commands/status_command.py @@ -9,7 +9,7 @@ from __future__ import annotations from datetime import datetime, timezone from typing import Any -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/sun_command.py b/modules/commands/sun_command.py index 6e5de58..1d45b50 100644 --- a/modules/commands/sun_command.py +++ b/modules/commands/sun_command.py @@ -3,7 +3,7 @@ Sun Command - Provides sunrise/sunset information """ -from ..models import MeshMessage +from shared.models import MeshMessage from ..solar_conditions import get_sun from .base_command import BaseCommand diff --git a/modules/commands/test_command.py b/modules/commands/test_command.py index 1dcab2c..9dbb228 100644 --- a/modules/commands/test_command.py +++ b/modules/commands/test_command.py @@ -9,7 +9,7 @@ import re from datetime import datetime from typing import Any, Optional -from ..models import MeshMessage +from shared.models import MeshMessage from ..response_template import format_piped_template from ..utils import calculate_distance, extract_path_node_ids_from_message from .base_command import BaseCommand diff --git a/modules/commands/trace_command.py b/modules/commands/trace_command.py index 3d016b3..0716369 100644 --- a/modules/commands/trace_command.py +++ b/modules/commands/trace_command.py @@ -9,7 +9,7 @@ import re from typing import Optional from ..graph_trace_helper import update_mesh_graph_from_trace_data -from ..models import MeshMessage +from shared.models import MeshMessage from ..trace_runner import RunTraceResult, run_trace from .base_command import BaseCommand diff --git a/modules/commands/version_command.py b/modules/commands/version_command.py index 25193c6..93f7127 100644 --- a/modules/commands/version_command.py +++ b/modules/commands/version_command.py @@ -6,7 +6,7 @@ Returns the currently running bot version string. from typing import Any -from ..models import MeshMessage +from shared.models import MeshMessage from ..version_info import resolve_runtime_version from .base_command import BaseCommand diff --git a/modules/commands/webviewer_command.py b/modules/commands/webviewer_command.py index 0a39a5a..a796364 100644 --- a/modules/commands/webviewer_command.py +++ b/modules/commands/webviewer_command.py @@ -4,7 +4,7 @@ Web Viewer Command Provides commands to manage the web viewer integration """ -from ..models import MeshMessage +from shared.models import MeshMessage from .base_command import BaseCommand diff --git a/modules/commands/wx_command.py b/modules/commands/wx_command.py index 2b38754..9896bfb 100644 --- a/modules/commands/wx_command.py +++ b/modules/commands/wx_command.py @@ -13,7 +13,7 @@ import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry -from ..models import MeshMessage +from shared.models import MeshMessage from ..utils import ( format_temperature_high_low, geocode_city_sync, diff --git a/modules/core.py b/modules/core.py index d66e287..6c84966 100644 --- a/modules/core.py +++ b/modules/core.py @@ -27,7 +27,7 @@ from meshcore import EventType from .channel_manager import ChannelManager from .command_manager import CommandManager -from .db_manager import AsyncDBManager, DBManager +from shared.db_manager import AsyncDBManager, DBManager from .feed_manager import FeedManager from .i18n import Translator from .message_handler import MessageHandler diff --git a/modules/feed_manager.py b/modules/feed_manager.py index d97b5a8..7fe0051 100644 --- a/modules/feed_manager.py +++ b/modules/feed_manager.py @@ -22,7 +22,7 @@ import aiohttp import feedparser from modules.feed_filter_eval import item_passes_filter_config -from modules.security_utils import sanitize_input, validate_external_url +from shared.security_utils import sanitize_input, validate_external_url from modules.url_shortener import _coerce_url_string, shorten_url_sync diff --git a/modules/maintenance.py b/modules/maintenance.py index cd74492..410d00a 100644 --- a/modules/maintenance.py +++ b/modules/maintenance.py @@ -11,7 +11,7 @@ import time from pathlib import Path from typing import TYPE_CHECKING, Any, Callable -from .security_utils import validate_external_url +from shared.security_utils import validate_external_url if TYPE_CHECKING: pass diff --git a/modules/message_handler.py b/modules/message_handler.py index a198d3e..de47dd3 100644 --- a/modules/message_handler.py +++ b/modules/message_handler.py @@ -14,8 +14,8 @@ from typing import Any, TypedDict from .enums import AdvertFlags, DeviceRole, PayloadType, PayloadVersion, RouteType from .graph_trace_helper import update_mesh_graph_from_trace_data -from .models import MeshMessage -from .security_utils import sanitize_input, sanitize_name +from shared.models import MeshMessage +from shared.security_utils import sanitize_input, sanitize_name from .utils import ( calculate_packet_hash, decode_path_len_byte, diff --git a/modules/repeater_manager.py b/modules/repeater_manager.py index e418858..6e4f93a 100644 --- a/modules/repeater_manager.py +++ b/modules/repeater_manager.py @@ -12,7 +12,7 @@ from typing import Any, NamedTuple, Optional from meshcore import EventType -from .security_utils import sanitize_name, validate_pubkey_format +from shared.security_utils import sanitize_name, validate_pubkey_format from .utils import rate_limited_nominatim_reverse_sync diff --git a/modules/scheduler.py b/modules/scheduler.py index c1be95f..4f3ade7 100644 --- a/modules/scheduler.py +++ b/modules/scheduler.py @@ -25,7 +25,7 @@ from .scheduled_message_cron import ( parse_schedule_key, parse_scheduled_message_value, ) -from .security_utils import validate_external_url +from shared.security_utils import validate_external_url from .utils import decode_escape_sequences, format_keyword_response_with_placeholders, get_config_timezone diff --git a/modules/service_plugins/discord_bridge_service.py b/modules/service_plugins/discord_bridge_service.py index 487807d..375cb48 100644 --- a/modules/service_plugins/discord_bridge_service.py +++ b/modules/service_plugins/discord_bridge_service.py @@ -36,7 +36,7 @@ except ImportError: import contextlib from ..profanity_filter import censor, contains_profanity -from ..security_utils import sanitize_name +from shared.security_utils import sanitize_name from .base_service import BaseServicePlugin diff --git a/modules/web_viewer/app.py b/modules/web_viewer/app.py index 10eb54f..7d4b5be 100644 --- a/modules/web_viewer/app.py +++ b/modules/web_viewer/app.py @@ -42,7 +42,7 @@ from flask import ( ) from flask_socketio import SocketIO, disconnect, emit -from modules.security_utils import ( +from shared.security_utils import ( VALID_JOURNAL_MODES, validate_external_url, validate_sql_identifier, @@ -367,7 +367,7 @@ class BotDataViewer: """Initialize database connections""" try: # Initialize database manager for metadata access - from modules.db_manager import DBManager + from shared.db_manager import DBManager # Create a minimal bot object for DBManager class MinimalBot: def __init__(self, logger, config, db_manager=None): diff --git a/pyproject.toml b/pyproject.toml index ada13c8..4be926c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,8 @@ meshcore-viewer = "modules.web_viewer.app:main" [tool.setuptools] # Include both the main module and the modules package py-modules = ["meshcore_bot"] -packages = ["modules", "modules.commands", "modules.commands.alternatives", +packages = ["shared", "shared.parsers", + "modules", "modules.commands", "modules.commands.alternatives", "modules.commands.alternatives.inactive", "modules.service_plugins", "modules.web_viewer"] [tool.setuptools.package-data] @@ -130,7 +131,7 @@ module = [ "modules.message_handler", "modules.utils", "modules.plugin_loader", - "modules.security_utils", + "shared.security_utils", "modules.commands.base_command", ] disallow_untyped_defs = true diff --git a/shared/__init__.py b/shared/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/db_manager.py b/shared/db_manager.py similarity index 100% rename from modules/db_manager.py rename to shared/db_manager.py diff --git a/modules/db_migrations.py b/shared/db_migrations.py similarity index 100% rename from modules/db_migrations.py rename to shared/db_migrations.py diff --git a/modules/models.py b/shared/models.py similarity index 100% rename from modules/models.py rename to shared/models.py diff --git a/shared/parsers/__init__.py b/shared/parsers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/security_utils.py b/shared/security_utils.py similarity index 100% rename from modules/security_utils.py rename to shared/security_utils.py diff --git a/tests/commands/test_base_command.py b/tests/commands/test_base_command.py index 4cebf41..ac83183 100644 --- a/tests/commands/test_base_command.py +++ b/tests/commands/test_base_command.py @@ -10,7 +10,7 @@ from modules.commands.joke_command import JokeCommand from modules.commands.ping_command import PingCommand from modules.commands.sports_command import SportsCommand from modules.commands.stats_command import StatsCommand -from modules.models import CHANNEL_REGIONAL_FLOOD_SCOPE_BODY_OVERHEAD, MeshMessage +from shared.models import CHANNEL_REGIONAL_FLOOD_SCOPE_BODY_OVERHEAD, MeshMessage from tests.conftest import mock_message diff --git a/tests/conftest.py b/tests/conftest.py index 81db824..4ae7a52 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,9 +13,9 @@ from unittest.mock import AsyncMock, MagicMock, Mock import pytest -from modules.db_manager import DBManager +from shared.db_manager import DBManager from modules.mesh_graph import MeshGraph -from modules.models import MeshMessage +from shared.models import MeshMessage from tests.helpers import create_test_edge, populate_test_graph diff --git a/tests/integration/test_flood_scope_reply.py b/tests/integration/test_flood_scope_reply.py index 6f06694..fd5b98b 100644 --- a/tests/integration/test_flood_scope_reply.py +++ b/tests/integration/test_flood_scope_reply.py @@ -16,7 +16,7 @@ import pytest from modules.command_manager import CommandManager from modules.message_handler import MessageHandler -from modules.models import MeshMessage +from shared.models import MeshMessage # ── helpers ────────────────────────────────────────────────────────────────── diff --git a/tests/test_command_manager.py b/tests/test_command_manager.py index 9340e39..64a24d9 100644 --- a/tests/test_command_manager.py +++ b/tests/test_command_manager.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from modules.command_manager import CommandManager, InternetStatusCache -from modules.models import MeshMessage +from shared.models import MeshMessage from tests.conftest import mock_message diff --git a/tests/test_command_prefix.py b/tests/test_command_prefix.py index c699fc0..fd3cdec 100644 --- a/tests/test_command_prefix.py +++ b/tests/test_command_prefix.py @@ -14,7 +14,7 @@ from modules.command_manager import CommandManager from modules.commands.base_command import BaseCommand from modules.commands.hello_command import HelloCommand from modules.commands.ping_command import PingCommand -from modules.models import MeshMessage +from shared.models import MeshMessage class MockTestCommand(BaseCommand): diff --git a/tests/test_db_manager.py b/tests/test_db_manager.py index e58c48b..5509766 100644 --- a/tests/test_db_manager.py +++ b/tests/test_db_manager.py @@ -6,7 +6,7 @@ from unittest.mock import Mock import pytest -from modules.db_manager import DBManager +from shared.db_manager import DBManager @pytest.fixture diff --git a/tests/test_db_migrations.py b/tests/test_db_migrations.py index 70ee962..29d1382 100644 --- a/tests/test_db_migrations.py +++ b/tests/test_db_migrations.py @@ -5,7 +5,7 @@ import sqlite3 import pytest -from modules.db_migrations import ( +from shared.db_migrations import ( MIGRATIONS, MigrationRunner, _add_column, diff --git a/tests/test_feed_manager.py b/tests/test_feed_manager.py index d4d4691..b05a196 100644 --- a/tests/test_feed_manager.py +++ b/tests/test_feed_manager.py @@ -8,7 +8,7 @@ from unittest.mock import MagicMock, Mock import pytest -from modules.db_manager import DBManager +from shared.db_manager import DBManager from modules.feed_manager import FeedManager diff --git a/tests/test_feed_manager_extended.py b/tests/test_feed_manager_extended.py index 424e957..b62601a 100644 --- a/tests/test_feed_manager_extended.py +++ b/tests/test_feed_manager_extended.py @@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest -from modules.db_manager import DBManager +from shared.db_manager import DBManager from modules.feed_manager import FeedManager diff --git a/tests/test_message_handler.py b/tests/test_message_handler.py index 93efcf6..f10e20f 100644 --- a/tests/test_message_handler.py +++ b/tests/test_message_handler.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from modules.message_handler import MessageHandler -from modules.models import MeshMessage +from shared.models import MeshMessage from tests.conftest import mock_message as make_message diff --git a/tests/test_models.py b/tests/test_models.py index ddac3a2..0669c3d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,7 @@ """Tests for modules/models.py — MeshMessage dataclass.""" -from modules.models import MeshMessage +from shared.models import MeshMessage class TestMeshMessageDefaults: diff --git a/tests/test_plugin_loader.py b/tests/test_plugin_loader.py index 92c49e8..3c6b971 100644 --- a/tests/test_plugin_loader.py +++ b/tests/test_plugin_loader.py @@ -167,7 +167,7 @@ class TestCategoryAndFailed: # Minimal local plugin source (valid BaseCommand subclass) _LOCAL_PLUGIN_SOURCE = ''' from modules.commands.base_command import BaseCommand -from modules.models import MeshMessage +from shared.models import MeshMessage class HelloLocalCommand(BaseCommand): diff --git a/tests/test_security_utils.py b/tests/test_security_utils.py index 0d3a71b..1d24bec 100644 --- a/tests/test_security_utils.py +++ b/tests/test_security_utils.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest -from modules.security_utils import ( +from shared.security_utils import ( sanitize_input, validate_api_key_format, validate_external_url, @@ -43,7 +43,7 @@ class TestValidatePubkeyFormat: class TestValidateSafePath: """Tests for validate_safe_path().""" - @patch("modules.security_utils._is_nix_environment", return_value=True) + @patch("shared.security_utils._is_nix_environment", return_value=True) def test_relative_path_resolution(self, mock_nix, tmp_path): # Patch Nix check so tmp_path (under /private on macOS) doesn't trigger dangerous path result = validate_safe_path("subdir/file.db", base_dir=str(tmp_path), allow_absolute=False) @@ -57,7 +57,7 @@ class TestValidateSafePath: with pytest.raises(ValueError, match="Path traversal"): validate_safe_path("/etc/passwd", base_dir=str(tmp_path), allow_absolute=False) - @patch("modules.security_utils._is_nix_environment", return_value=True) + @patch("shared.security_utils._is_nix_environment", return_value=True) def test_absolute_path_when_allowed(self, mock_nix, tmp_path): target = tmp_path / "data" / "file.db" target.parent.mkdir(parents=True, exist_ok=True) @@ -233,26 +233,26 @@ class TestIsNixEnvironment: def test_nix_env_var_enables_dangerous_path_access(self, tmp_path): # When NIX_STORE is set, system-path check is skipped - import modules.security_utils as su + import shared.security_utils as su with patch.object(su, "_is_nix_environment", return_value=True): # /proc is dangerous on Linux, but Nix mode should allow it via allow_absolute result = validate_safe_path(str(tmp_path), base_dir=str(tmp_path), allow_absolute=True) assert result is not None def test_non_nix_env_detects_nix_store_var(self): - import modules.security_utils as su + import shared.security_utils as su with patch.dict(os.environ, {"NIX_STORE": "/nix/store"}, clear=False): assert su._is_nix_environment() is True def test_non_nix_env_detects_nix_path_var(self): - import modules.security_utils as su + import shared.security_utils as su env = {k: v for k, v in os.environ.items() if k not in ("NIX_STORE", "NIX_PATH", "NIX_REMOTE", "IN_NIX_SHELL")} with patch.dict(os.environ, {**env, "NIX_PATH": "/nix"}, clear=True): assert su._is_nix_environment() is True def test_no_nix_vars_returns_false(self): - import modules.security_utils as su + import shared.security_utils as su env = {k: v for k, v in os.environ.items() if k not in ("NIX_STORE", "NIX_PATH", "NIX_REMOTE", "IN_NIX_SHELL")} with patch.dict(os.environ, env, clear=True): @@ -282,13 +282,13 @@ class TestValidateSafePathExtra: """Additional coverage for validate_safe_path() exception paths.""" def test_dangerous_system_path_rejected_on_linux(self, tmp_path): - import modules.security_utils as su + import shared.security_utils as su with patch.object(su, "_is_nix_environment", return_value=False): with pytest.raises(ValueError, match="system directory"): validate_safe_path("/etc/passwd", allow_absolute=True) def test_unexpected_exception_wrapped_as_value_error(self, tmp_path): - with patch("modules.security_utils.Path.resolve", side_effect=OSError("disk fail")): + with patch("shared.security_utils.Path.resolve", side_effect=OSError("disk fail")): with pytest.raises(ValueError, match="Invalid or unsafe file path"): validate_safe_path("some_file.db", base_dir=str(tmp_path)) @@ -297,39 +297,39 @@ class TestSanitizeName: """Tests for sanitize_name() — log-safe identifier sanitization.""" def test_newline_stripped(self): - from modules.security_utils import sanitize_name + from shared.security_utils import sanitize_name assert "\n" not in sanitize_name("Evil\nNode") def test_carriage_return_stripped(self): - from modules.security_utils import sanitize_name + from shared.security_utils import sanitize_name assert "\r" not in sanitize_name("Evil\rNode") def test_tab_stripped(self): - from modules.security_utils import sanitize_name + from shared.security_utils import sanitize_name assert "\t" not in sanitize_name("Tab\tNode") def test_null_byte_stripped(self): - from modules.security_utils import sanitize_name + from shared.security_utils import sanitize_name assert "\x00" not in sanitize_name("Bad\x00Name") def test_ansi_escape_stripped(self): - from modules.security_utils import sanitize_name + from shared.security_utils import sanitize_name assert "\x1b" not in sanitize_name("\x1b[31mRed\x1b[0m") def test_truncated_to_max_length(self): - from modules.security_utils import sanitize_name + from shared.security_utils import sanitize_name result = sanitize_name("A" * 100, max_length=64) assert len(result) <= 64 def test_normal_name_unchanged(self): - from modules.security_utils import sanitize_name + from shared.security_utils import sanitize_name assert sanitize_name("Alice") == "Alice" def test_non_string_coerced(self): - from modules.security_utils import sanitize_name + from shared.security_utils import sanitize_name assert sanitize_name(42) == "42" def test_negative_max_length_raises(self): - from modules.security_utils import sanitize_name + from shared.security_utils import sanitize_name with pytest.raises(ValueError): sanitize_name("test", max_length=-1) diff --git a/tests/test_web_viewer.py b/tests/test_web_viewer.py index d387bc9..7280fc5 100644 --- a/tests/test_web_viewer.py +++ b/tests/test_web_viewer.py @@ -2326,7 +2326,7 @@ class TestBotIntegrationQueue: bot.bot_root = str(tmp_path) # Ensure schema exists (packet_stream is migration-owned). - from modules.db_manager import DBManager + from shared.db_manager import DBManager class MinimalBot: def __init__(self, logger, config): @@ -2358,7 +2358,7 @@ class TestBotIntegrationQueue: bot.config = cfg bot.bot_root = str(tmp_path) - from modules.db_manager import DBManager + from shared.db_manager import DBManager class MinimalBot: def __init__(self, logger, config): diff --git a/tests/unit/test_flood_scope_resolve.py b/tests/unit/test_flood_scope_resolve.py index 55dd04d..a5ff746 100644 --- a/tests/unit/test_flood_scope_resolve.py +++ b/tests/unit/test_flood_scope_resolve.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from modules.command_manager import CommandManager -from modules.models import MeshMessage +from shared.models import MeshMessage from modules.service_plugins.base_service import BaseServicePlugin diff --git a/tests/unit/test_path_command_multibyte.py b/tests/unit/test_path_command_multibyte.py index c1883b1..151cca7 100644 --- a/tests/unit/test_path_command_multibyte.py +++ b/tests/unit/test_path_command_multibyte.py @@ -9,7 +9,7 @@ from unittest.mock import MagicMock, Mock import pytest from modules.commands.path_command import PathCommand -from modules.models import MeshMessage +from shared.models import MeshMessage @pytest.mark.unit diff --git a/tests/unit/test_path_command_utf8_message_limits.py b/tests/unit/test_path_command_utf8_message_limits.py index 14db0d2..bdfe619 100644 --- a/tests/unit/test_path_command_utf8_message_limits.py +++ b/tests/unit/test_path_command_utf8_message_limits.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from modules.commands.path_command import PathCommand -from modules.models import MeshMessage +from shared.models import MeshMessage @pytest.mark.unit diff --git a/tests/unit/test_response_template.py b/tests/unit/test_response_template.py index c2ae19c..fd92565 100644 --- a/tests/unit/test_response_template.py +++ b/tests/unit/test_response_template.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, Mock import pytest from modules.commands.test_command import TestCommand as MeshTestCommand -from modules.models import MeshMessage +from shared.models import MeshMessage from modules.response_template import format_piped_template from modules.utils import message_path_bytes_per_hop