From 6363c8dd914f58dad2830ba66aaaa9a89585028c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Sun, 14 Jul 2024 18:52:06 -0400 Subject: [PATCH] Add JS8Call capability --- .gitignore | 1 + command_handlers.py | 27 +--- js8call_integration.py | 296 +++++++++++++++++++++++++++++++++++++++++ message_processing.py | 13 +- server.py | 33 ++++- 5 files changed, 339 insertions(+), 31 deletions(-) create mode 100644 js8call_integration.py diff --git a/.gitignore b/.gitignore index 2b8a1e0..739b0d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ __pycache__/ bulletins.db +js8call.db venv/ .venv .idea diff --git a/command_handlers.py b/command_handlers.py index 9f19109..46bb9ad 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -21,7 +21,7 @@ def handle_help_command(sender_id, interface, menu_name=None): if menu_name: update_user_state(sender_id, {'command': 'MENU', 'menu': menu_name, 'step': 1}) if menu_name == 'bbs': - response = "📰BBS Menu📰\n[M]ail\n[B]ulletins\n[C]hannel Dir\nE[X]IT" + response = "📰BBS Menu📰\n[M]ail\n[B]ulletins\n[C]hannel Dir\n[J]S8CALL\nE[X]IT" elif menu_name == 'utilities': response = "🛠️Utilities Menu🛠️\n[S]tats\n[F]ortune\n[W]all of Shame\nE[X]IT" else: @@ -30,8 +30,6 @@ def handle_help_command(sender_id, interface, menu_name=None): send_message(response, sender_id, interface) - - def get_node_name(node_id, interface): node_info = interface.nodes.get(node_id) if node_info: @@ -605,29 +603,8 @@ def handle_list_channels_command(sender_id, interface): logging.error(f"Error processing list channels command: {e}") send_message("Error processing list channels command.", sender_id, interface) -def handle_read_channel_command(sender_id, message, state, interface): - try: - channels = state.get('channels', []) - message_number = int(message) - 1 - - if message_number < 0 or message_number >= len(channels): - send_message("Invalid channel number. Please try again.", sender_id, interface) - return - - channel_name, channel_url = channels[message_number] - response = f"Channel Name: {channel_name}\nChannel URL: {channel_url}" - send_message(response, sender_id, interface) - - update_user_state(sender_id, None) - - except ValueError: - send_message("Invalid input. Please enter a valid channel number.", sender_id, interface) - except Exception as e: - logging.error(f"Error processing read channel command: {e}") - send_message("Error processing read channel command.", sender_id, interface) - def handle_quick_help_command(sender_id, interface): response = ("✈️QUICK COMMANDS✈️\nSend command below for usage info:\nSM,, - Send " "Mail\nCM - Check Mail\nPB,, - Post Bulletin\nCB,, - Check Bulletins\n") - send_message(response, sender_id, interface) \ No newline at end of file + send_message(response, sender_id, interface) diff --git a/js8call_integration.py b/js8call_integration.py new file mode 100644 index 0000000..46783ff --- /dev/null +++ b/js8call_integration.py @@ -0,0 +1,296 @@ +from socket import socket, AF_INET, SOCK_STREAM +import json +import time +import sqlite3 +import configparser +import logging + +from meshtastic import BROADCAST_NUM + +from command_handlers import handle_help_command +from utils import send_message, update_user_state + +config_file = 'config.ini' + +def from_message(content): + try: + return json.loads(content) + except ValueError: + return {} + +def to_message(typ, value='', params=None): + if params is None: + params = {} + return json.dumps({'type': typ, 'value': value, 'params': params}) + + +class JS8CallClient: + def __init__(self, interface, logger=None): + self.logger = logger or logging.getLogger('js8call') + self.logger.setLevel(logging.INFO) + self.config = configparser.ConfigParser() + self.config.read(config_file) + + self.server = ( + self.config.get('js8call', 'host', fallback=None), + self.config.getint('js8call', 'port', fallback=None) + ) + self.db_file = self.config.get('js8call', 'db_file', fallback=None) + self.js8groups = self.config.get('js8call', 'js8groups', fallback='').split(',') + self.store_messages = self.config.getboolean('js8call', 'store_messages', fallback=True) + self.js8urgent = self.config.get('js8call', 'js8urgent', fallback='').split(',') + self.js8groups = [group.strip() for group in self.js8groups] + self.js8urgent = [group.strip() for group in self.js8urgent] + + self.connected = False + self.sock = None + self.db_conn = None + self.interface = interface + + if self.db_file: + self.db_conn = sqlite3.connect(self.db_file) + self.create_tables() + else: + self.logger.info("JS8Call configuration not found. Skipping JS8Call integration.") + + def create_tables(self): + if not self.db_conn: + return + + with self.db_conn: + self.db_conn.execute(''' + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sender TEXT, + receiver TEXT, + message TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + self.db_conn.execute(''' + CREATE TABLE IF NOT EXISTS groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sender TEXT, + groupname TEXT, + message TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + self.db_conn.execute(''' + CREATE TABLE IF NOT EXISTS urgent ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sender TEXT, + groupname TEXT, + message TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + self.logger.info("Database tables created or verified.") + + def insert_message(self, sender, receiver, message): + if not self.db_conn: + self.logger.error("Database connection is not available.") + return + + try: + with self.db_conn: + self.db_conn.execute(''' + INSERT INTO messages (sender, receiver, message) + VALUES (?, ?, ?) + ''', (sender, receiver, message)) + self.logger.info(f"Message inserted: {sender} to {receiver} - {message}") + except sqlite3.Error as e: + self.logger.error(f"Failed to insert message into database: {e}") + + def insert_group(self, sender, groupname, message): + if not self.db_conn: + self.logger.error("Database connection is not available.") + return + + try: + with self.db_conn: + self.db_conn.execute(''' + INSERT INTO groups (sender, groupname, message) + VALUES (?, ?, ?) + ''', (sender, groupname, message)) + except sqlite3.Error as e: + self.logger.error(f"Failed to insert group message into database: {e}") + + def insert_urgent(self, sender, groupname, message): + if not self.db_conn: + self.logger.error("Database connection is not available.") + return + + try: + with self.db_conn: + self.db_conn.execute(''' + INSERT INTO urgent (sender, groupname, message) + VALUES (?, ?, ?) + ''', (sender, groupname, message)) + self.logger.info(f"Urgent message inserted: {sender} to {groupname} - {message}") + except sqlite3.Error as e: + self.logger.error(f"Failed to insert urgent message into database: {e}") + + def process(self, message): + typ = message.get('type', '') + value = message.get('value', '') + params = message.get('params', {}) + + if not typ: + return + + rx_types = [ + 'RX.ACTIVITY', 'RX.DIRECTED', 'RX.SPOT', 'RX.CALL_ACTIVITY', + 'RX.CALL_SELECTED', 'RX.DIRECTED_ME', 'RX.ECHO', 'RX.DIRECTED_GROUP', + 'RX.META', 'RX.MSG', 'RX.PING', 'RX.PONG', 'RX.STREAM' + ] + + if typ not in rx_types: + return + + if typ == 'RX.DIRECTED' and value: + parts = value.split(' ') + if len(parts) < 3: + self.logger.warning(f"Unexpected message format: {value}") + return + + sender = parts[0] + receiver = parts[1] + msg = ' '.join(parts[2:]).strip() + + self.logger.info(f"Received JS8Call message: {sender} to {receiver} - {msg}") + + if receiver in self.js8urgent: + self.insert_urgent(sender, receiver, msg) + notification_message = f"💥 URGENT JS8Call Message Received 💥\nFrom: {sender}\nCheck BBS for message" + send_message(notification_message, BROADCAST_NUM, self.interface) + elif receiver in self.js8groups: + self.insert_group(sender, receiver, msg) + elif self.store_messages: + self.insert_message(sender, receiver, msg) + else: + pass + + def send(self, *args, **kwargs): + params = kwargs.get('params', {}) + if '_ID' not in params: + params['_ID'] = '{}'.format(int(time.time() * 1000)) + kwargs['params'] = params + message = to_message(*args, **kwargs) + self.sock.send((message + '\n').encode('utf-8')) # Convert to bytes + + def connect(self): + if not self.server[0] or not self.server[1]: + self.logger.info("JS8Call server configuration not found. Skipping JS8Call connection.") + return + + self.logger.info(f"Connecting to {self.server}") + self.sock = socket(AF_INET, SOCK_STREAM) + try: + self.sock.connect(self.server) + self.connected = True + self.send("STATION.GET_STATUS") + + while self.connected: + content = self.sock.recv(65500).decode('utf-8') # Decode received bytes to string + if not content: + continue # Skip empty content + + try: + message = json.loads(content) + except ValueError: + continue # Skip invalid JSON content + + if not message: + continue # Skip empty message + + self.process(message) + except ConnectionRefusedError: + self.logger.error(f"Connection to JS8Call server {self.server} refused.") + finally: + self.sock.close() + + def close(self): + self.connected = False + + + +def handle_js8call_command(sender_id, interface): + response = "JS8Call Menu:\n[G]roup Messages\n[S]tation Messages\n[U]rgent Messages\nE[X]IT" + send_message(response, sender_id, interface) + update_user_state(sender_id, {'command': 'JS8CALL_MENU', 'step': 1}) + +def handle_js8call_steps(sender_id, message, step, interface, state): + if step == 1: + choice = message.lower() + if choice == 'x': + handle_help_command(sender_id, interface, 'bbs') + return + elif choice == 'g': + handle_group_messages_command(sender_id, interface) + elif choice == 's': + handle_station_messages_command(sender_id, interface) + elif choice == 'u': + handle_urgent_messages_command(sender_id, interface) + else: + send_message("Invalid option. Please choose again.", sender_id, interface) + handle_js8call_command(sender_id, interface) + +def handle_group_messages_command(sender_id, interface): + conn = sqlite3.connect('js8call.db') + c = conn.cursor() + c.execute("SELECT DISTINCT groupname FROM groups") + groups = c.fetchall() + if groups: + response = "Group Messages Menu:\n" + "\n".join([f"[{i}] {group[0]}" for i, group in enumerate(groups)]) + send_message(response, sender_id, interface) + update_user_state(sender_id, {'command': 'GROUP_MESSAGES', 'step': 1, 'groups': groups}) + else: + send_message("No group messages available.", sender_id, interface) + handle_js8call_command(sender_id, interface) + +def handle_station_messages_command(sender_id, interface): + conn = sqlite3.connect('js8call.db') + c = conn.cursor() + c.execute("SELECT sender, receiver, message, timestamp FROM messages") + messages = c.fetchall() + if messages: + response = "Station Messages:\n" + "\n".join([f"[{i+1}] {msg[0]} -> {msg[1]}: {msg[2]} ({msg[3]})" for i, msg in enumerate(messages)]) + send_message(response, sender_id, interface) + else: + send_message("No station messages available.", sender_id, interface) + handle_js8call_command(sender_id, interface) + +def handle_urgent_messages_command(sender_id, interface): + conn = sqlite3.connect('js8call.db') + c = conn.cursor() + c.execute("SELECT sender, groupname, message, timestamp FROM urgent") + messages = c.fetchall() + if messages: + response = "Urgent Messages:\n" + "\n".join([f"[{i+1}] {msg[0]} -> {msg[1]}: {msg[2]} ({msg[3]})" for i, msg in enumerate(messages)]) + send_message(response, sender_id, interface) + else: + send_message("No urgent messages available.", sender_id, interface) + handle_js8call_command(sender_id, interface) + +def handle_group_message_selection(sender_id, message, step, state, interface): + groups = state['groups'] + try: + group_index = int(message) + groupname = groups[group_index][0] + + conn = sqlite3.connect('js8call.db') + c = conn.cursor() + c.execute("SELECT sender, message, timestamp FROM groups WHERE groupname=?", (groupname,)) + messages = c.fetchall() + + if messages: + response = f"Messages for group {groupname}:\n" + "\n".join([f"[{i+1}] {msg[0]}: {msg[1]} ({msg[2]})" for i, msg in enumerate(messages)]) + send_message(response, sender_id, interface) + else: + send_message(f"No messages for group {groupname}.", sender_id, interface) + except (IndexError, ValueError): + send_message("Invalid group selection. Please choose again.", sender_id, interface) + handle_group_messages_command(sender_id, interface) + + handle_js8call_command(sender_id, interface) diff --git a/message_processing.py b/message_processing.py index 03bc56f..57eade1 100644 --- a/message_processing.py +++ b/message_processing.py @@ -11,6 +11,7 @@ from command_handlers import ( handle_post_channel_command, handle_list_channels_command, handle_quick_help_command ) 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 main_menu_handlers = { @@ -24,6 +25,7 @@ bbs_menu_handlers = { "m": handle_mail_command, "b": handle_bulletin_command, "c": handle_channel_directory_command, + "j": handle_js8call_command, "x": handle_help_command } @@ -107,6 +109,12 @@ def process_message(sender_id, message, interface, is_sync_message=False): handlers = bulletin_menu_handlers elif state and state['command'] == 'BULLETIN_ACTION': handlers = board_action_handlers + elif state and state['command'] == 'JS8CALL_MENU': + handle_js8call_steps(sender_id, message, state['step'], interface, state) + return + elif state and state['command'] == 'GROUP_MESSAGES': + handle_group_message_selection(sender_id, message, state['step'], state, interface) + return else: handlers = main_menu_handlers @@ -152,10 +160,13 @@ 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 == 'JS8CALL_MENU': + handle_js8call_steps(sender_id, message, step, interface, state) + elif command == 'GROUP_MESSAGES': + handle_group_message_selection(sender_id, message, step, state, interface) else: handle_help_command(sender_id, interface) - def on_receive(packet, interface): try: if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP': diff --git a/server.py b/server.py index b0f1123..177f82f 100644 --- a/server.py +++ b/server.py @@ -2,8 +2,8 @@ """ TC²-BBS Server for Meshtastic by TheCommsChannel (TC²) -Date: 07/09/2024 -Version: 0.1.4 +Date: 07/14/2024 +Version: 0.1.6 Description: The system allows for mail message handling, bulletin boards, and a channel @@ -13,15 +13,29 @@ other BBS servers listed in the config.ini file. """ import logging +import time from config_init import initialize_config, get_interface, init_cli_parser, merge_config from db_operations import initialize_database +from js8call_integration import JS8CallClient from message_processing import on_receive from pubsub import pub -import time -# Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +# General logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) + +# JS8Call logging +js8call_logger = logging.getLogger('js8call') +js8call_logger.setLevel(logging.DEBUG) +js8call_handler = logging.StreamHandler() +js8call_handler.setLevel(logging.DEBUG) +js8call_formatter = logging.Formatter('%(asctime)s - JS8Call - %(levelname)s - %(message)s', '%Y-%m-%d %H:%M:%S') +js8call_handler.setFormatter(js8call_formatter) +js8call_logger.addHandler(js8call_handler) def display_banner(): banner = """ @@ -58,6 +72,13 @@ def main(): pub.subscribe(receive_packet, system_config['mqtt_topic']) + # Initialize and start JS8Call Client if configured + js8call_client = JS8CallClient(interface) + js8call_client.logger = js8call_logger + + if js8call_client.db_conn: + js8call_client.connect() + try: while True: time.sleep(1) @@ -65,6 +86,8 @@ def main(): except KeyboardInterrupt: logging.info("Shutting down the server...") interface.close() + if js8call_client.connected: + js8call_client.close() if __name__ == "__main__": main()