From 4ac47e84bf601c4aad30e4ca548e3399a2dc665b Mon Sep 17 00:00:00 2001 From: default Date: Thu, 3 Apr 2025 19:13:40 +0000 Subject: [PATCH 1/3] Update weather plugin and enhance command handling - Added weather plugin with functionality to fetch and display weather information using the OpenWeather API. - Integrated weather command into the main menu and updated command handlers to manage user interactions for weather queries. - Updated .gitignore to include database and configuration files. - Created a test client for interactive testing of the BBS system. - Added example configuration for the weather plugin. --- .gitignore | 4 +- command_handlers.py | 8 ++ example_config.ini | 1 + message_processing.py | 11 +++ test_client.py | 124 ++++++++++++++++++++++++ weather_plugin/Weather.md | 75 +++++++++++++++ weather_plugin/__init__.py | 9 ++ weather_plugin/command_router.py | 29 ++++++ weather_plugin/menu.py | 7 ++ weather_plugin/weater.ini.example | 12 +++ weather_plugin/weather.py | 154 ++++++++++++++++++++++++++++++ 11 files changed, 432 insertions(+), 2 deletions(-) create mode 100644 test_client.py create mode 100644 weather_plugin/Weather.md create mode 100644 weather_plugin/__init__.py create mode 100644 weather_plugin/command_router.py create mode 100644 weather_plugin/menu.py create mode 100644 weather_plugin/weater.ini.example create mode 100644 weather_plugin/weather.py diff --git a/.gitignore b/.gitignore index 739b0d7..d93e110 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ __pycache__/ -bulletins.db -js8call.db +*.db venv/ .venv .idea config.ini +*/*.ini fortunes.txt diff --git a/command_handlers.py b/command_handlers.py index 5696d0f..682155c 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -4,6 +4,7 @@ import random import time from meshtastic import BROADCAST_NUM +from weather_plugin.menu import get_weather_menu_text from db_operations import ( add_bulletin, add_mail, delete_mail, @@ -24,6 +25,7 @@ config.read('config.ini') main_menu_items = config['menu']['main_menu_items'].split(',') bbs_menu_items = config['menu']['bbs_menu_items'].split(',') utilities_menu_items = config['menu']['utilities_menu_items'].split(',') +plugin_menu_items = config['menu']['plugin_menu_items'].split(',') def build_menu(items, menu_name): @@ -38,6 +40,8 @@ def build_menu(items, menu_name): menu_str += "[B]BS\n" elif item.strip() == 'U': menu_str += "[U]tilities\n" + elif item.strip() == 'P': + menu_str += "[P]lugins\n" elif item.strip() == 'X': menu_str += "E[X]IT\n" elif item.strip() == 'M': @@ -52,6 +56,8 @@ def build_menu(items, menu_name): menu_str += "[F]ortune\n" elif item.strip() == 'W': menu_str += "[W]all of Shame\n" + elif item.strip() == 'WE': + menu_str += get_weather_menu_text() return menu_str def handle_help_command(sender_id, interface, menu_name=None): @@ -61,6 +67,8 @@ def handle_help_command(sender_id, interface, menu_name=None): response = build_menu(bbs_menu_items, "📰BBS Menu📰") elif menu_name == 'utilities': response = build_menu(utilities_menu_items, "🛠️Utilities Menu🛠️") + elif menu_name == 'plugins': + response = build_menu(plugin_menu_items, "🔌Plugins Menu🔌") else: update_user_state(sender_id, {'command': 'MAIN_MENU', 'step': 1}) # Reset to main menu state mail = get_mail(get_node_id_from_num(sender_id, interface)) diff --git a/example_config.ini b/example_config.ini index 3066b36..22e58e0 100644 --- a/example_config.ini +++ b/example_config.ini @@ -81,6 +81,7 @@ bbs_menu_items = M, B, C, J, X # Remove any menu items from the list below that you want to exclude from the utilities menu utilities_menu_items = S, F, W, X +plugin_menu_items = WE ########################## #### JS8Call Settings #### diff --git a/message_processing.py b/message_processing.py index 41bd7d7..0d954aa 100644 --- a/message_processing.py +++ b/message_processing.py @@ -10,6 +10,7 @@ from command_handlers import ( handle_check_bulletin_command, handle_read_bulletin_command, handle_read_channel_command, handle_post_channel_command, handle_list_channels_command, handle_quick_help_command ) +from weather_plugin.command_router import handle_weather_command, handle_weather_steps from db_operations import add_bulletin, add_mail, delete_bulletin, delete_mail, get_db_connection, add_channel from js8call_integration import handle_js8call_command, handle_js8call_steps, handle_group_message_selection from utils import get_user_state, get_node_short_name, get_node_id_from_num, send_message @@ -18,6 +19,12 @@ main_menu_handlers = { "q": handle_quick_help_command, "b": lambda sender_id, interface: handle_help_command(sender_id, interface, 'bbs'), "u": lambda sender_id, interface: handle_help_command(sender_id, interface, 'utilities'), + "p": lambda sender_id, interface: handle_help_command(sender_id, interface, 'plugins'), + "x": handle_help_command +} + +plugins_menu_handlers = { + "we": handle_weather_command, "x": handle_help_command } @@ -109,6 +116,8 @@ def process_message(sender_id, message, interface, is_sync_message=False): handlers = bbs_menu_handlers elif menu_name == 'utilities': handlers = utilities_menu_handlers + elif menu_name == 'plugins': + handlers = plugins_menu_handlers else: handlers = main_menu_handlers elif state and state['command'] == 'BULLETIN_MENU': @@ -166,6 +175,8 @@ def process_message(sender_id, message, interface, is_sync_message=False): handle_bb_steps(sender_id, message, 5, state, interface, bbs_nodes) elif command == 'BULLETIN_READ': handle_bb_steps(sender_id, message, 3, state, interface, bbs_nodes) + elif command == 'WEATHER': + handle_weather_steps(sender_id, message, step, interface) elif command == 'JS8CALL_MENU': handle_js8call_steps(sender_id, message, step, interface, state) elif command == 'GROUP_MESSAGES': diff --git a/test_client.py b/test_client.py new file mode 100644 index 0000000..388726f --- /dev/null +++ b/test_client.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "meshtastic", +# "pypubsub" ] +# /// + +# You can use Astral UV to use this script. +# https://docs.astral.sh/uv/getting-started/installation/ +# Then simply run uv run test_client.py + +import logging +import time +import uuid +from datetime import datetime + +from command_handlers import handle_help_command +from db_operations import get_db_connection, initialize_database +from message_processing import process_message +from utils import get_user_state, send_message + +class MockInterface: + def __init__(self): + # Initialize with test nodes that match the BBS system structure + self.nodes = { + "!test1": { + "num": 1234, + "user": {"shortName": "TEST1", "longName": "Test Node 1"}, + "fromId": "!test1" + }, + "!test2": { + "num": 5678, + "user": {"shortName": "TEST2", "longName": "Test Node 2"}, + "fromId": "!test2" + } + } + # BBS nodes for syncing bulletins and mail + self.bbs_nodes = ["!test1", "!test2"] + self.messages_sent = [] + + # Initialize database for testing + initialize_database() + + class MockResponse: + def __init__(self, id): + self.id = id + + def sendText(self, text, destinationId, wantAck=True, wantResponse=False): + message = { + "text": text, + "destinationId": destinationId, + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "id": str(uuid.uuid4()) + } + self.messages_sent.append(message) + return self.MockResponse(message['id']) + + def get_sent_messages(self): + return self.messages_sent + +def run_interactive_test(): + # Initialize test environment + interface = MockInterface() + test_user_id = "!test1" + + def process_and_show_response(command): + # Create a packet similar to what Meshtastic would send + packet = { + 'from': interface.nodes[test_user_id]['num'], # The sender's node number + 'fromId': test_user_id, # The sender's node ID + 'decoded': { + 'portnum': 'TEXT_MESSAGE_APP', + 'payload': command.encode('utf-8') + } + } + # Process the message using the packet + process_message(packet['from'], command, interface) + print("\nSystem Response:") + for msg in interface.get_sent_messages(): + print(f"{msg['text']}") + interface.messages_sent.clear() + print("\n" + "-" * 50) + + print("\n=== Interactive BBS Test Client ===") + print("You are logged in as TEST1 (Node ID: !test1)") + print("\nAvailable Commands:") + print("- 'x' for main menu") + print("- Quick commands:") + print(" * SM,,recipient,subject,message - Send mail") + print(" * PB,,board,subject,message - Post bulletin") + print("- 'quit' to exit") + print("-" * 50) + + # Show initial menu + process_and_show_response("x") + + while True: + try: + command = input("\nEnter command: ").strip() + if command.lower() == 'quit': + print("\nExiting test client...") + break + + process_and_show_response(command) + + except KeyboardInterrupt: + print("\nExiting test client...") + break + except Exception as e: + print(f"Error: {str(e)}") + +if __name__ == "__main__": + # Set up logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + try: + run_interactive_test() + except Exception as e: + logging.error(f"Test failed with error: {str(e)}") + print("\nTest client exited with error.") \ No newline at end of file diff --git a/weather_plugin/Weather.md b/weather_plugin/Weather.md new file mode 100644 index 0000000..db478d7 --- /dev/null +++ b/weather_plugin/Weather.md @@ -0,0 +1,75 @@ +# Weather Plugin Documentation + +## Overview +The Weather Plugin provides weather information for the EDC LV BBS system using the OpenWeather API. It allows users to query current weather conditions by ZIP code. + +## Structure +``` +weather_plugin/ +├── __init__.py # Plugin exports and initialization +├── weather.py # Core weather functionality and API interaction +├── command_router.py # Command handling and user interaction +├── menu.py # Menu text and options +└── weather.ini # Configuration file +``` + +## Components + +### weather.py +Core functionality: +- OpenWeather API integration +- Rate limiting (per minute and daily) +- HTTP request handling with retries +- Temperature conversion (Kelvin to Fahrenheit) +- Weather data formatting + +Key features: +- Configurable API rate limits +- Robust error handling +- Request retries for reliability +- Session management for connection reuse + +### command_router.py +User interaction: +- Handles weather command from main menu +- Processes ZIP code input +- Manages user state +- Returns formatted weather responses + +### menu.py +Menu integration: +- Provides menu text for weather option +- Defines menu item identifier + +### Configuration (weather.ini) +Settings: +- OpenWeather API key +- Rate limiting parameters: + - Max calls per minute + - Max calls per day +- Plugin enabled/disabled flag + +## Usage Flow +1. User selects "[WE]ather" from main menu +2. System prompts for ZIP code +3. User enters 5-digit ZIP code +4. System checks rate limits +5. Makes API request if within limits +6. Returns formatted weather data: + - Temperature + - Feels like temperature + - Humidity + - Weather conditions + - City name + +## Error Handling +- Invalid ZIP codes +- API rate limiting +- Network timeouts +- Missing configuration +- API errors + +## Dependencies +- requests: HTTP client library +- configparser: Configuration file handling +- utils: BBS system utilities \ No newline at end of file diff --git a/weather_plugin/__init__.py b/weather_plugin/__init__.py new file mode 100644 index 0000000..204ebb5 --- /dev/null +++ b/weather_plugin/__init__.py @@ -0,0 +1,9 @@ +from .command_router import handle_weather_command, handle_weather_steps +from .menu import get_weather_menu_item, get_weather_menu_text + +__all__ = [ + 'handle_weather_command', + 'handle_weather_steps', + 'get_weather_menu_item', + 'get_weather_menu_text' +] \ No newline at end of file diff --git a/weather_plugin/command_router.py b/weather_plugin/command_router.py new file mode 100644 index 0000000..be46594 --- /dev/null +++ b/weather_plugin/command_router.py @@ -0,0 +1,29 @@ +from utils import send_message, update_user_state +from .weather import get_weather_by_zip, format_weather_response + +def handle_weather_command(sender_id, interface): + """Handle the weather command from the main menu.""" + response = "🌡️ Weather Service 🌡️\n\nEnter a ZIP code (89115) to get the current weather, or 'X' to exit:" + send_message(response, sender_id, interface) + update_user_state(sender_id, {'command': 'WEATHER', 'step': 1}) + +def handle_weather_steps(sender_id, message, step, interface): + """Handle the steps for getting weather information.""" + message = message.strip().upper() + + if message == 'X': + from command_handlers import handle_help_command + handle_help_command(sender_id, interface) + return + + if step == 1: + if not message.isdigit() or len(message) != 5: + send_message("Please enter a valid 5-digit ZIP code, or 'X' to exit.", sender_id, interface) + return + + weather_data = get_weather_by_zip(message) + response = format_weather_response(weather_data) + send_message(response, sender_id, interface) + + from command_handlers import handle_help_command + handle_help_command(sender_id, interface) \ No newline at end of file diff --git a/weather_plugin/menu.py b/weather_plugin/menu.py new file mode 100644 index 0000000..8b3f1ae --- /dev/null +++ b/weather_plugin/menu.py @@ -0,0 +1,7 @@ +def get_weather_menu_item(): + """Return the weather menu item for the main menu.""" + return "WE" + +def get_weather_menu_text(): + """Return the weather menu text.""" + return "[WE]ather\n" \ No newline at end of file diff --git a/weather_plugin/weater.ini.example b/weather_plugin/weater.ini.example new file mode 100644 index 0000000..525b968 --- /dev/null +++ b/weather_plugin/weater.ini.example @@ -0,0 +1,12 @@ +########################## +#### OpenWeather API #### +########################## +# API key and rate limiting settings for the OpenWeather API +# You can get one here.. https://openweathermap.org/api +[openweather] +api_key = YOUR API KEY HERE +# Maximum number of API calls per minute (default: 30) +max_calls_per_minute = 30 +# Maximum number of API calls per day (default: 500) +max_calls_per_day = 500 +enabled = true diff --git a/weather_plugin/weather.py b/weather_plugin/weather.py new file mode 100644 index 0000000..c2df6ed --- /dev/null +++ b/weather_plugin/weather.py @@ -0,0 +1,154 @@ +""" +Weather Module for EDC LV BBS + +This module provides weather information using the OpenWeather API. +""" + +import logging +import os +import requests +import time +from datetime import datetime, timedelta +from configparser import ConfigParser +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +# OpenWeather API configuration +config = ConfigParser() +config_path = os.path.join(os.path.dirname(__file__), 'weather.ini') + +if not os.path.exists(config_path): + logging.error(f"Weather configuration file not found at {config_path}") + raise FileNotFoundError(f"Weather configuration file not found at {config_path}") + +try: + config.read(config_path) +except Exception as e: + logging.error(f"Error reading weather configuration: {e}") + raise + +OPENWEATHER_API_KEY = config.get('openweather', 'api_key', fallback='') +WEATHER_API_URL = "http://api.openweathermap.org/data/2.5/weather" + +# Rate limiting configuration +MAX_CALLS_PER_MINUTE = int(config.get('openweather', 'max_calls_per_minute', fallback='30')) +MAX_CALLS_PER_DAY = int(config.get('openweather', 'max_calls_per_day', fallback='500')) + +# Create a session object for better connection handling +session = requests.Session() +retry_strategy = Retry( + total=3, # number of retries + backoff_factor=1, # wait 1, 2, 4 seconds between retries + status_forcelist=[500, 502, 503, 504] # HTTP status codes to retry on +) +adapter = HTTPAdapter(max_retries=retry_strategy) +session.mount("http://", adapter) +session.mount("https://", adapter) + +# Rate limiting state +minute_calls = 0 +day_calls = 0 +last_minute = datetime.now().replace(second=0, microsecond=0) +last_day = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + +def check_rate_limits(): + """ + Check if we're within rate limits. Updates counters and resets them when needed. + + Returns: + bool: True if we can make a call, False if we've hit a limit + """ + global minute_calls, day_calls, last_minute, last_day + + now = datetime.now() + current_minute = now.replace(second=0, microsecond=0) + current_day = now.replace(hour=0, minute=0, second=0, microsecond=0) + + # Reset minute counter if we're in a new minute + if current_minute > last_minute: + minute_calls = 0 + last_minute = current_minute + + # Reset day counter if we're in a new day + if current_day > last_day: + day_calls = 0 + last_day = current_day + + # Check if we've hit any limits + if minute_calls >= MAX_CALLS_PER_MINUTE: + logging.warning(f"Weather API minute limit reached ({MAX_CALLS_PER_MINUTE} calls)") + return False + + if day_calls >= MAX_CALLS_PER_DAY: + logging.warning(f"Weather API daily limit reached ({MAX_CALLS_PER_DAY} calls)") + return False + + # We're good to make a call + minute_calls += 1 + day_calls += 1 + return True + +def kelvin_to_fahrenheit(kelvin): + """Convert Kelvin to Fahrenheit.""" + return (kelvin - 273.15) * 9/5 + 32 + +def get_weather_by_zip(zip_code, country_code='US'): + """ + Get weather information from OpenWeather API by zip code. + + Args: + zip_code: ZIP code to get weather for + country_code: Country code (default: US) + + Returns: + dict: Weather data if successful, None if failed + """ + if not OPENWEATHER_API_KEY: + logging.error("OpenWeather API key not configured") + return None + + # Check rate limits before making the call + if not check_rate_limits(): + logging.error("Weather API rate limit exceeded") + return None + + try: + params = { + 'zip': f"{zip_code},{country_code}", + 'appid': OPENWEATHER_API_KEY + } + response = session.get(WEATHER_API_URL, params=params, timeout=5) + response.raise_for_status() + return response.json() + except requests.Timeout: + logging.error(f"Weather API request timed out after 5 seconds for zip code {zip_code}") + return {"error": "Request timed out - weather service is taking too long to respond"} + except requests.exceptions.RequestException as e: + logging.error(f"Error fetching weather data: {e}") + return None + +def format_weather_response(weather_data): + """Format weather data into a user-friendly message.""" + if not weather_data: + return "❌ Failed to get weather data." + + if "error" in weather_data: + return f"❌ {weather_data['error']}" + + try: + temp = kelvin_to_fahrenheit(weather_data['main']['temp']) + feels_like = kelvin_to_fahrenheit(weather_data['main']['feels_like']) + humidity = weather_data['main']['humidity'] + description = weather_data['weather'][0]['description'].capitalize() + city = weather_data['name'] + + response = f"🌡️ Weather for {city} 🌡️\n\n" + response += f"Temperature: {temp:.1f}°F\n" + response += f"Feels like: {feels_like:.1f}°F\n" + response += f"Humidity: {humidity}%\n" + response += f"Conditions: {description}\n" + + return response + except (KeyError, IndexError) as e: + logging.error(f"Error formatting weather data: {e}") + return "❌ Error processing weather data" \ No newline at end of file From 3e3bc7f1f63ac03b82b6ced45deb890067b46e2f Mon Sep 17 00:00:00 2001 From: default Date: Thu, 3 Apr 2025 19:20:18 +0000 Subject: [PATCH 2/3] Update weather plugin and enhance command handling - Added weather plugin with functionality to fetch and display weather information using the OpenWeather API. - Integrated weather command into the main menu and updated command handlers to manage user interactions for weather queries. - Updated .gitignore to include database and configuration files. - Created a test client for interactive testing of the BBS system. - Added example configuration for the weather plugin. --- weather_plugin/{Weather.md => README.md} | 22 +++++++++++++++------- weather_plugin/weather.py | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) rename weather_plugin/{Weather.md => README.md} (66%) diff --git a/weather_plugin/Weather.md b/weather_plugin/README.md similarity index 66% rename from weather_plugin/Weather.md rename to weather_plugin/README.md index db478d7..c09b2f6 100644 --- a/weather_plugin/Weather.md +++ b/weather_plugin/README.md @@ -1,7 +1,7 @@ # Weather Plugin Documentation ## Overview -The Weather Plugin provides weather information for the EDC LV BBS system using the OpenWeather API. It allows users to query current weather conditions by ZIP code. +The Weather Plugin provides weather information for the TC² BBS system using the OpenWeather API. It allows users to query current weather conditions by ZIP code. The plugin is accessible through the Plugins Menu in the BBS system. ## Structure ``` @@ -49,18 +49,26 @@ Settings: - Max calls per day - Plugin enabled/disabled flag +## Menu Integration +The Weather Plugin is integrated into the TC² BBS system through the Plugins Menu: +- Located in the main menu under [P]lugins +- Identified by "WE" in the plugins menu +- Configurable through the plugin_menu_items setting in config.ini + ## Usage Flow -1. User selects "[WE]ather" from main menu -2. System prompts for ZIP code -3. User enters 5-digit ZIP code -4. System checks rate limits -5. Makes API request if within limits -6. Returns formatted weather data: +1. User selects [P]lugins from main menu +2. User selects "[WE]ather" from plugins menu +3. System prompts for ZIP code +4. User enters 5-digit ZIP code +5. System checks rate limits +6. Makes API request if within limits +7. Returns formatted weather data: - Temperature - Feels like temperature - Humidity - Weather conditions - City name +8. Returns to plugins menu after displaying weather ## Error Handling - Invalid ZIP codes diff --git a/weather_plugin/weather.py b/weather_plugin/weather.py index c2df6ed..1d37a03 100644 --- a/weather_plugin/weather.py +++ b/weather_plugin/weather.py @@ -1,5 +1,5 @@ """ -Weather Module for EDC LV BBS +Weather Module for TC² BBS This module provides weather information using the OpenWeather API. """ From 24f329d07e4785a7846ae7c6590a670ce6bef62e Mon Sep 17 00:00:00 2001 From: default Date: Thu, 3 Apr 2025 20:45:56 +0000 Subject: [PATCH 3/3] Refactor weather plugin structure and update imports - Moved weather plugin files into a new 'plugins' directory for better organization. - Updated import statements in command_handlers.py and message_processing.py to reflect the new file structure. - Adjusted .gitignore to exclude nested .ini files. - Removed obsolete weather plugin files and documentation as part of the restructuring. --- .gitignore | 2 +- command_handlers.py | 2 +- message_processing.py | 4 ++-- plugins/__init__.py | 0 {weather_plugin => plugins/weather_plugin}/README.md | 0 {weather_plugin => plugins/weather_plugin}/__init__.py | 0 {weather_plugin => plugins/weather_plugin}/command_router.py | 0 {weather_plugin => plugins/weather_plugin}/menu.py | 0 .../weather_plugin/weather.ini.example | 0 {weather_plugin => plugins/weather_plugin}/weather.py | 0 10 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 plugins/__init__.py rename {weather_plugin => plugins/weather_plugin}/README.md (100%) rename {weather_plugin => plugins/weather_plugin}/__init__.py (100%) rename {weather_plugin => plugins/weather_plugin}/command_router.py (100%) rename {weather_plugin => plugins/weather_plugin}/menu.py (100%) rename weather_plugin/weater.ini.example => plugins/weather_plugin/weather.ini.example (100%) rename {weather_plugin => plugins/weather_plugin}/weather.py (100%) diff --git a/.gitignore b/.gitignore index d93e110..fcde568 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,5 @@ venv/ .venv .idea config.ini -*/*.ini +*/*/*.ini fortunes.txt diff --git a/command_handlers.py b/command_handlers.py index 682155c..732a4bf 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -4,7 +4,7 @@ import random import time from meshtastic import BROADCAST_NUM -from weather_plugin.menu import get_weather_menu_text +from plugins.weather_plugin.menu import get_weather_menu_text from db_operations import ( add_bulletin, add_mail, delete_mail, diff --git a/message_processing.py b/message_processing.py index 0d954aa..d4a16b0 100644 --- a/message_processing.py +++ b/message_processing.py @@ -10,10 +10,10 @@ from command_handlers import ( handle_check_bulletin_command, handle_read_bulletin_command, handle_read_channel_command, handle_post_channel_command, handle_list_channels_command, handle_quick_help_command ) -from weather_plugin.command_router import handle_weather_command, handle_weather_steps +from plugins.weather_plugin.command_router import handle_weather_command, handle_weather_steps from db_operations import add_bulletin, add_mail, delete_bulletin, delete_mail, get_db_connection, add_channel from js8call_integration import handle_js8call_command, handle_js8call_steps, handle_group_message_selection -from utils import get_user_state, get_node_short_name, get_node_id_from_num, send_message +from utils import get_node_short_name, get_node_id_from_num, send_message, get_user_state, update_user_state main_menu_handlers = { "q": handle_quick_help_command, diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/weather_plugin/README.md b/plugins/weather_plugin/README.md similarity index 100% rename from weather_plugin/README.md rename to plugins/weather_plugin/README.md diff --git a/weather_plugin/__init__.py b/plugins/weather_plugin/__init__.py similarity index 100% rename from weather_plugin/__init__.py rename to plugins/weather_plugin/__init__.py diff --git a/weather_plugin/command_router.py b/plugins/weather_plugin/command_router.py similarity index 100% rename from weather_plugin/command_router.py rename to plugins/weather_plugin/command_router.py diff --git a/weather_plugin/menu.py b/plugins/weather_plugin/menu.py similarity index 100% rename from weather_plugin/menu.py rename to plugins/weather_plugin/menu.py diff --git a/weather_plugin/weater.ini.example b/plugins/weather_plugin/weather.ini.example similarity index 100% rename from weather_plugin/weater.ini.example rename to plugins/weather_plugin/weather.ini.example diff --git a/weather_plugin/weather.py b/plugins/weather_plugin/weather.py similarity index 100% rename from weather_plugin/weather.py rename to plugins/weather_plugin/weather.py