mirror of
https://github.com/TheCommsChannel/TC2-BBS-mesh.git
synced 2025-09-21 05:24:58 -04:00
Merge 24f329d07e
into 295fb35c92
This commit is contained in:
commit
e262987f7d
12 changed files with 441 additions and 3 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,8 +1,8 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
bulletins.db
|
*.db
|
||||||
js8call.db
|
|
||||||
venv/
|
venv/
|
||||||
.venv
|
.venv
|
||||||
.idea
|
.idea
|
||||||
config.ini
|
config.ini
|
||||||
|
*/*/*.ini
|
||||||
fortunes.txt
|
fortunes.txt
|
||||||
|
|
|
@ -4,6 +4,7 @@ import random
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from meshtastic import BROADCAST_NUM
|
from meshtastic import BROADCAST_NUM
|
||||||
|
from plugins.weather_plugin.menu import get_weather_menu_text
|
||||||
|
|
||||||
from db_operations import (
|
from db_operations import (
|
||||||
add_bulletin, add_mail, delete_mail,
|
add_bulletin, add_mail, delete_mail,
|
||||||
|
@ -24,6 +25,7 @@ config.read('config.ini')
|
||||||
main_menu_items = config['menu']['main_menu_items'].split(',')
|
main_menu_items = config['menu']['main_menu_items'].split(',')
|
||||||
bbs_menu_items = config['menu']['bbs_menu_items'].split(',')
|
bbs_menu_items = config['menu']['bbs_menu_items'].split(',')
|
||||||
utilities_menu_items = config['menu']['utilities_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):
|
def build_menu(items, menu_name):
|
||||||
|
@ -38,6 +40,8 @@ def build_menu(items, menu_name):
|
||||||
menu_str += "[B]BS\n"
|
menu_str += "[B]BS\n"
|
||||||
elif item.strip() == 'U':
|
elif item.strip() == 'U':
|
||||||
menu_str += "[U]tilities\n"
|
menu_str += "[U]tilities\n"
|
||||||
|
elif item.strip() == 'P':
|
||||||
|
menu_str += "[P]lugins\n"
|
||||||
elif item.strip() == 'X':
|
elif item.strip() == 'X':
|
||||||
menu_str += "E[X]IT\n"
|
menu_str += "E[X]IT\n"
|
||||||
elif item.strip() == 'M':
|
elif item.strip() == 'M':
|
||||||
|
@ -52,6 +56,8 @@ def build_menu(items, menu_name):
|
||||||
menu_str += "[F]ortune\n"
|
menu_str += "[F]ortune\n"
|
||||||
elif item.strip() == 'W':
|
elif item.strip() == 'W':
|
||||||
menu_str += "[W]all of Shame\n"
|
menu_str += "[W]all of Shame\n"
|
||||||
|
elif item.strip() == 'WE':
|
||||||
|
menu_str += get_weather_menu_text()
|
||||||
return menu_str
|
return menu_str
|
||||||
|
|
||||||
def handle_help_command(sender_id, interface, menu_name=None):
|
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📰")
|
response = build_menu(bbs_menu_items, "📰BBS Menu📰")
|
||||||
elif menu_name == 'utilities':
|
elif menu_name == 'utilities':
|
||||||
response = build_menu(utilities_menu_items, "🛠️Utilities Menu🛠️")
|
response = build_menu(utilities_menu_items, "🛠️Utilities Menu🛠️")
|
||||||
|
elif menu_name == 'plugins':
|
||||||
|
response = build_menu(plugin_menu_items, "🔌Plugins Menu🔌")
|
||||||
else:
|
else:
|
||||||
update_user_state(sender_id, {'command': 'MAIN_MENU', 'step': 1}) # Reset to main menu state
|
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))
|
mail = get_mail(get_node_id_from_num(sender_id, interface))
|
||||||
|
|
|
@ -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
|
# Remove any menu items from the list below that you want to exclude from the utilities menu
|
||||||
utilities_menu_items = S, F, W, X
|
utilities_menu_items = S, F, W, X
|
||||||
|
|
||||||
|
plugin_menu_items = WE
|
||||||
|
|
||||||
##########################
|
##########################
|
||||||
#### JS8Call Settings ####
|
#### JS8Call Settings ####
|
||||||
|
|
|
@ -10,14 +10,21 @@ from command_handlers import (
|
||||||
handle_check_bulletin_command, handle_read_bulletin_command, handle_read_channel_command,
|
handle_check_bulletin_command, handle_read_bulletin_command, handle_read_channel_command,
|
||||||
handle_post_channel_command, handle_list_channels_command, handle_quick_help_command
|
handle_post_channel_command, handle_list_channels_command, handle_quick_help_command
|
||||||
)
|
)
|
||||||
|
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 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 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 = {
|
main_menu_handlers = {
|
||||||
"q": handle_quick_help_command,
|
"q": handle_quick_help_command,
|
||||||
"b": lambda sender_id, interface: handle_help_command(sender_id, interface, 'bbs'),
|
"b": lambda sender_id, interface: handle_help_command(sender_id, interface, 'bbs'),
|
||||||
"u": lambda sender_id, interface: handle_help_command(sender_id, interface, 'utilities'),
|
"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
|
"x": handle_help_command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +116,8 @@ def process_message(sender_id, message, interface, is_sync_message=False):
|
||||||
handlers = bbs_menu_handlers
|
handlers = bbs_menu_handlers
|
||||||
elif menu_name == 'utilities':
|
elif menu_name == 'utilities':
|
||||||
handlers = utilities_menu_handlers
|
handlers = utilities_menu_handlers
|
||||||
|
elif menu_name == 'plugins':
|
||||||
|
handlers = plugins_menu_handlers
|
||||||
else:
|
else:
|
||||||
handlers = main_menu_handlers
|
handlers = main_menu_handlers
|
||||||
elif state and state['command'] == 'BULLETIN_MENU':
|
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)
|
handle_bb_steps(sender_id, message, 5, state, interface, bbs_nodes)
|
||||||
elif command == 'BULLETIN_READ':
|
elif command == 'BULLETIN_READ':
|
||||||
handle_bb_steps(sender_id, message, 3, state, interface, bbs_nodes)
|
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':
|
elif command == 'JS8CALL_MENU':
|
||||||
handle_js8call_steps(sender_id, message, step, interface, state)
|
handle_js8call_steps(sender_id, message, step, interface, state)
|
||||||
elif command == 'GROUP_MESSAGES':
|
elif command == 'GROUP_MESSAGES':
|
||||||
|
|
0
plugins/__init__.py
Normal file
0
plugins/__init__.py
Normal file
83
plugins/weather_plugin/README.md
Normal file
83
plugins/weather_plugin/README.md
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
# Weather Plugin Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
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
|
||||||
|
```
|
||||||
|
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
|
||||||
|
|
||||||
|
## 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 [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
|
||||||
|
- API rate limiting
|
||||||
|
- Network timeouts
|
||||||
|
- Missing configuration
|
||||||
|
- API errors
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- requests: HTTP client library
|
||||||
|
- configparser: Configuration file handling
|
||||||
|
- utils: BBS system utilities
|
9
plugins/weather_plugin/__init__.py
Normal file
9
plugins/weather_plugin/__init__.py
Normal file
|
@ -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'
|
||||||
|
]
|
29
plugins/weather_plugin/command_router.py
Normal file
29
plugins/weather_plugin/command_router.py
Normal file
|
@ -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)
|
7
plugins/weather_plugin/menu.py
Normal file
7
plugins/weather_plugin/menu.py
Normal file
|
@ -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"
|
12
plugins/weather_plugin/weather.ini.example
Normal file
12
plugins/weather_plugin/weather.ini.example
Normal file
|
@ -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
|
154
plugins/weather_plugin/weather.py
Normal file
154
plugins/weather_plugin/weather.py
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
"""
|
||||||
|
Weather Module for TC² 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"
|
124
test_client.py
Normal file
124
test_client.py
Normal file
|
@ -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.")
|
Loading…
Add table
Add a link
Reference in a new issue