From b4477eebe94cd48189ded3325ae2b60dd5f8a36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Sat, 29 Jun 2024 17:33:09 -0400 Subject: [PATCH 01/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26249c7..4c3602f 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ If you would like to have the script automatically run at boot, follow the steps sudo systemctl start mesh-bbs.service ``` - The service should be started now and should start anytime your device is powered on or rebooted. You can check the status ofk the service by running the following command: + The service should be started now and should start anytime your device is powered on or rebooted. You can check the status of the service by running the following command: ```sh sudo systemctl status mesh-bbs.service From db71bd771bb3d2dc329b692ffc168dd6b99caa3d Mon Sep 17 00:00:00 2001 From: Vince Loschiavo Date: Sat, 29 Jun 2024 16:33:51 -0700 Subject: [PATCH 02/30] Fix for bulletin issue - Info and News was swapped on line 151 [issue #28] --- command_handlers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command_handlers.py b/command_handlers.py index 8e81266..0f7e008 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -148,7 +148,7 @@ def handle_stats_steps(sender_id, message, step, interface, bbs_nodes): def handle_bb_steps(sender_id, message, step, state, interface, bbs_nodes): - boards = {0: "General", 1: "News", 2: "Info", 3: "Urgent"} + boards = {0: "General", 1: "Info", 2: "News", 3: "Urgent"} if step == 1: if message == '4': @@ -397,4 +397,4 @@ def handle_channel_directory_steps(sender_id, message, step, state, interface): channel_name = state['channel_name'] add_channel(channel_name, channel_url) send_message(f"Your channel '{channel_name}' has been added to the directory.", sender_id, interface) - handle_channel_directory_command(sender_id, interface) \ No newline at end of file + handle_channel_directory_command(sender_id, interface) From 538ccd2f9712ec71b78b36229969c6cc37ae42d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Sun, 30 Jun 2024 05:04:34 -0400 Subject: [PATCH 03/30] Update README.md Created working role list and added Router_Client role. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c3602f..8393b9e 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,11 @@ If you would like to have the script automatically run at boot, follow the steps ``` ## Radio Configuration -Note: Radio device role must be set to **CLIENT**, other roles may allow the BBS to communicate for a short time, but then the BBS will stop responding to requests +Note: There have been reports of issues with some device roles that may allow the BBS to communicate for a short time, but then the BBS will stop responding to requests. + +The following device roles that have been tested and working: +- **Client** +- **Router_Client** ## Features From ced378bef37c2a7b1ed1a675acf86f664264fc2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Sun, 30 Jun 2024 09:55:11 -0400 Subject: [PATCH 04/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8393b9e..67eb4c2 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ If you would like to have the script automatically run at boot, follow the steps Note: There have been reports of issues with some device roles that may allow the BBS to communicate for a short time, but then the BBS will stop responding to requests. -The following device roles that have been tested and working: +The following device roles have been working: - **Client** - **Router_Client** From 5935c4d27094b280e3ded3dcfdfbed46bdc2e6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Tue, 2 Jul 2024 08:11:16 -0400 Subject: [PATCH 05/30] Change group_chat_id to use BROADCAST_NUM This change is to use BROADCAST_NUM from the Meshtastic Python lib instead of broadcast address "4294967295" in the code --- db_operations.py | 5 +++-- message_processing.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/db_operations.py b/db_operations.py index e613ffa..2325bb7 100644 --- a/db_operations.py +++ b/db_operations.py @@ -4,6 +4,8 @@ import threading import uuid from datetime import datetime +from meshtastic import BROADCAST_NUM + from utils import ( send_bulletin_to_bbs_nodes, send_delete_bulletin_to_bbs_nodes, @@ -78,9 +80,8 @@ def add_bulletin(board, sender_short_name, subject, content, bbs_nodes, interfac # New logic to send group chat notification for urgent bulletins if board.lower() == "urgent": - group_chat_id = 4294967295 # Default group chat ID (0xFFFFFFFF) notification_message = f"πŸ’₯NEW URGENT BULLETINπŸ’₯\nFrom: {sender_short_name}\nTitle: {subject}" - send_message(notification_message, group_chat_id, interface) + send_message(notification_message, BROADCAST_NUM, interface) return unique_id diff --git a/message_processing.py b/message_processing.py index 49dd1b0..88a18ce 100644 --- a/message_processing.py +++ b/message_processing.py @@ -1,5 +1,7 @@ import logging +from meshtastic import BROADCAST_NUM + from command_handlers import ( handle_mail_command, handle_bulletin_command, handle_exit_command, handle_help_command, handle_stats_command, handle_fortune_command, @@ -33,9 +35,8 @@ def process_message(sender_id, message, interface, is_sync_message=False): add_bulletin(board, sender_short_name, subject, content, [], interface, unique_id=unique_id) if board.lower() == "urgent": - group_chat_id = 4294967295 notification_message = f"πŸ’₯NEW URGENT BULLETINπŸ’₯\nFrom: {sender_short_name}\nTitle: {subject}" - send_message(notification_message, group_chat_id, interface) + send_message(notification_message, BROADCAST_NUM, interface) elif message.startswith("MAIL|"): parts = message.split("|") sender_id, sender_short_name, recipient_id, subject, content, unique_id = parts[1], parts[2], parts[3], parts[4], parts[5], parts[6] From be9aa375cf0ed8f96fb2a51e37f87698ff26d36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Tue, 2 Jul 2024 18:47:59 -0400 Subject: [PATCH 06/30] Update server.py Fix pubsub errors --- server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.py b/server.py index b590e63..af5117c 100644 --- a/server.py +++ b/server.py @@ -57,7 +57,7 @@ def main(): initialize_database() - def receive_packet(packet): + def receive_packet(packet, interface): on_receive(packet, interface) pub.subscribe(receive_packet, system_config['mqtt_topic']) From a3e0b91995a5e992c2ae7f821450c8e49196d99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Thu, 4 Jul 2024 06:41:30 -0400 Subject: [PATCH 07/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 67eb4c2..22259a2 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ If you would like to have the script automatically run at boot, follow the steps sudo systemctl stop mesh-bbs.service ``` - If you make changes to the watchlist.txt file, you will need to restart the service with the following command: + If you need to restart the service, you can do so with the following command: ```sh sudo systemctl restart mesh-bbs.service From ab9449ad0b0559e4366629df2396f6260a2bc1e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:30:34 -0400 Subject: [PATCH 08/30] A number of fixes - Added PyCharm .idea folder to gitignore - Made main menu banner alfa-numeric only to fix issue with iOS devices not being able to see the menu - Fixed delete-sync issue --- .gitignore | 1 + command_handlers.py | 2 +- config.ini | 2 +- db_operations.py | 9 +++++---- message_processing.py | 15 +++++++++++++-- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index e4d6245..adffc7d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__/ bulletins.db venv/ .venv +.idea diff --git a/command_handlers.py b/command_handlers.py index 0f7e008..c247133 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -41,7 +41,7 @@ def handle_exit_command(sender_id, interface): def handle_help_command(sender_id, interface, state=None): - title = "β–ˆβ–“β–’β–‘ TCΒ² BBS β–‘β–’β–“β–ˆ\n" + title = "TC2 BBS\n" commands = [ "[M]ail Menu", "[B]ulletin Menu", diff --git a/config.ini b/config.ini index 3dc354f..b953fd1 100644 --- a/config.ini +++ b/config.ini @@ -17,7 +17,7 @@ [interface] type = serial -# port = /dev/ttyUSB0 +# port = /dev/ttyACM0 # hostname = 192.168.x.x diff --git a/db_operations.py b/db_operations.py index 2325bb7..0f366c7 100644 --- a/db_operations.py +++ b/db_operations.py @@ -132,17 +132,16 @@ def get_mail_content(mail_id, recipient_id): return c.fetchone() def delete_mail(unique_id, recipient_id, bbs_nodes, interface): - # TODO: ensure only recipient can delete mail - logging.info(f"Attempting to delete mail with unique_id: {unique_id} by {recipient_id}") conn = get_db_connection() c = conn.cursor() try: - c.execute("SELECT unique_id FROM mail WHERE unique_id = ? and recipient = ?", (unique_id, recipient_id,)) + c.execute("SELECT recipient FROM mail WHERE unique_id = ?", (unique_id,)) result = c.fetchone() - logging.debug(f"Fetch result for unique_id {unique_id}: {result}") if result is None: logging.error(f"No mail found with unique_id: {unique_id}") return # Early exit if no matching mail found + recipient_id = result[0] + logging.info(f"Attempting to delete mail with unique_id: {unique_id} by {recipient_id}") c.execute("DELETE FROM mail WHERE unique_id = ? and recipient = ?", (unique_id, recipient_id,)) conn.commit() send_delete_mail_to_bbs_nodes(unique_id, bbs_nodes, interface) @@ -150,3 +149,5 @@ def delete_mail(unique_id, recipient_id, bbs_nodes, interface): except Exception as e: logging.error(f"Error deleting mail with unique_id {unique_id}: {e}") raise + + diff --git a/message_processing.py b/message_processing.py index 88a18ce..1efc252 100644 --- a/message_processing.py +++ b/message_processing.py @@ -9,7 +9,7 @@ from command_handlers import ( handle_channel_directory_command, handle_channel_directory_steps ) -from db_operations import add_bulletin, add_mail, delete_bulletin, delete_mail +from db_operations import add_bulletin, add_mail, delete_bulletin, delete_mail, get_db_connection from utils import get_user_state, get_node_short_name, get_node_id_from_num, send_message command_handlers = { @@ -47,7 +47,8 @@ def process_message(sender_id, message, interface, is_sync_message=False): elif message.startswith("DELETE_MAIL|"): unique_id = message.split("|")[1] logging.info(f"Processing delete mail with unique_id: {unique_id}") - delete_mail(unique_id, [], interface) + recipient_id = get_recipient_id_by_mail(unique_id) # Fetch the recipient_id using this helper function + delete_mail(unique_id, recipient_id, [], interface) else: if message_lower in command_handlers: command_handlers[message_lower](sender_id, interface) @@ -95,3 +96,13 @@ def on_receive(packet, interface): logging.info("Ignoring message sent to group chat or from unknown node") except KeyError as e: logging.error(f"Error processing packet: {e}") + +def get_recipient_id_by_mail(unique_id): + # Fix for Mail Delete sync issue + conn = get_db_connection() + c = conn.cursor() + c.execute("SELECT recipient FROM mail WHERE unique_id = ?", (unique_id,)) + result = c.fetchone() + if result: + return result[0] + return None From 8bf833daa95371d2eb4d7babfa78a1adc8310fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Thu, 4 Jul 2024 11:42:29 -0400 Subject: [PATCH 09/30] Make short_name case insensitive Make short_name case insensitive and added config.ini to gitignore, added example_config.ini, and updated README --- .gitignore | 1 + README.md | 8 +++++++- command_handlers.py | 2 +- config.ini | 6 +++--- example_config.ini | 34 ++++++++++++++++++++++++++++++++++ utils.py | 2 +- 6 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 example_config.ini diff --git a/.gitignore b/.gitignore index adffc7d..ec35470 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ bulletins.db venv/ .venv .idea +config.ini \ No newline at end of file diff --git a/README.md b/README.md index 22259a2..68730ba 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,13 @@ If you're a Docker user, TCΒ²-BBS Meshtastic is available on Docker Hub! pip install -r requirements.txt ``` -5. Set up the configuration in `config.ini`: +5. Rename `example_config.ini`: + + ```sh + mv example_config.ini config.ini + ``` + +6. Set up the configuration in `config.ini`: **[interface]** If using `type = serial` and you have multiple devices connected, you will need to uncomment the `port =` line and enter the port of your device. diff --git a/command_handlers.py b/command_handlers.py index c247133..dd59a0e 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -273,7 +273,7 @@ def handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes): update_user_state(sender_id, None) elif step == 3: - short_name = message + short_name = message.lower() nodes = get_node_info(interface, short_name) if not nodes: send_message("I'm unable to find that node in my database.", sender_id, interface) diff --git a/config.ini b/config.ini index b953fd1..39ac10f 100644 --- a/config.ini +++ b/config.ini @@ -17,7 +17,7 @@ [interface] type = serial -# port = /dev/ttyACM0 +port = COM3 # hostname = 192.168.x.x @@ -30,5 +30,5 @@ type = serial # [sync] # bbs_nodes = !17d7e4b7,!18e9f5a3,!1a2b3c4d -# [sync] -# bbs_nodes = !17d7e4b7 +[sync] +bbs_nodes = !f53f441f diff --git a/example_config.ini b/example_config.ini new file mode 100644 index 0000000..b953fd1 --- /dev/null +++ b/example_config.ini @@ -0,0 +1,34 @@ +############################### +#### Select Interface type #### +############################### +# [type = serial] for USB connected devices +#If there are multiple serial devices connected, be sure to use the "port" option and specify a port +# Linux Example: +# port = /dev/ttyUSB0 +# +# Windows Example: +# port = COM3 +# [type = tcp] for network connected devices (ESP32 devices only - this does not work for WisBlock) +# If using tcp, remove the # from the beginning and replace 192.168.x.x with the IP address of your device +# Example: +# [interface] +# type = tcp +# hostname = 192.168.1.100 + +[interface] +type = serial +# port = /dev/ttyACM0 +# hostname = 192.168.x.x + + +############################ +#### BBS NODE SYNC LIST #### +############################ +# Provide a list of other nodes running TCΒ²-BBS to sync mail messages and bulletins with +# Enter in a list of other BBS Nodes by their nodeID separated by commas (no spaces) +# Example: +# [sync] +# bbs_nodes = !17d7e4b7,!18e9f5a3,!1a2b3c4d + +# [sync] +# bbs_nodes = !17d7e4b7 diff --git a/utils.py b/utils.py index 460aea8..4fa3009 100644 --- a/utils.py +++ b/utils.py @@ -28,7 +28,7 @@ def send_message(message, destination, interface): def get_node_info(interface, short_name): nodes = [{'num': node_id, 'shortName': node['user']['shortName'], 'longName': node['user']['longName']} for node_id, node in interface.nodes.items() - if node['user']['shortName'] == short_name] + if node['user']['shortName'].lower() == short_name] return nodes From 56448a417a6b8c66089c0dec9115191b6a73d018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Thu, 4 Jul 2024 11:43:53 -0400 Subject: [PATCH 10/30] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ec35470..af2f5e3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ bulletins.db venv/ .venv .idea -config.ini \ No newline at end of file +config.ini From 783c434f3f2af6b7105769b98bb34a05722ea680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Thu, 4 Jul 2024 13:18:54 -0400 Subject: [PATCH 11/30] Delete config.ini --- config.ini | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 config.ini diff --git a/config.ini b/config.ini deleted file mode 100644 index 39ac10f..0000000 --- a/config.ini +++ /dev/null @@ -1,34 +0,0 @@ -############################### -#### Select Interface type #### -############################### -# [type = serial] for USB connected devices -#If there are multiple serial devices connected, be sure to use the "port" option and specify a port -# Linux Example: -# port = /dev/ttyUSB0 -# -# Windows Example: -# port = COM3 -# [type = tcp] for network connected devices (ESP32 devices only - this does not work for WisBlock) -# If using tcp, remove the # from the beginning and replace 192.168.x.x with the IP address of your device -# Example: -# [interface] -# type = tcp -# hostname = 192.168.1.100 - -[interface] -type = serial -port = COM3 -# hostname = 192.168.x.x - - -############################ -#### BBS NODE SYNC LIST #### -############################ -# Provide a list of other nodes running TCΒ²-BBS to sync mail messages and bulletins with -# Enter in a list of other BBS Nodes by their nodeID separated by commas (no spaces) -# Example: -# [sync] -# bbs_nodes = !17d7e4b7,!18e9f5a3,!1a2b3c4d - -[sync] -bbs_nodes = !f53f441f From aa6f70831ff04e92f108a0dd2f74ab84e8163403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Thu, 4 Jul 2024 23:46:22 -0400 Subject: [PATCH 12/30] Large number of changes - Changed menu structure to support Web Client - Added Quick Commands - Added Sync process for Channel Dir --- command_handlers.py | 295 ++++++++++++++++++++++++++++++++++++++++-- db_operations.py | 9 +- message_processing.py | 50 +++++-- utils.py | 6 + 4 files changed, 338 insertions(+), 22 deletions(-) diff --git a/command_handlers.py b/command_handlers.py index dd59a0e..6cd22fe 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -2,6 +2,8 @@ import logging import random import time +from meshtastic import BROADCAST_NUM + from config_init import initialize_config from db_operations import ( add_bulletin, add_mail, delete_mail, @@ -41,16 +43,14 @@ def handle_exit_command(sender_id, interface): def handle_help_command(sender_id, interface, state=None): - title = "TC2 BBS\n" + title = "πŸ’ΎTC2 BBSπŸ’Ύ\n" commands = [ - "[M]ail Menu", - "[B]ulletin Menu", - "[S]tats Menu", - "[F]ortune", - "[W]all of Shame", - "[C]hannel Directory", - "EXIT: Exit current menu", - "[H]elp" + "[QCH] - Quick Commands", + "[St]ats Menu", + "[Fo]rtune", + "[WS]Wall of Shame", + "[EXIT]", + "[HELP]" ] if state and 'command' in state: current_command = state['command'] @@ -322,8 +322,7 @@ def handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes): unique_id = add_mail(get_node_id_from_num(sender_id, interface), sender_short_name, recipient_id, subject, content, bbs_nodes, interface) send_message(f"Mail has been posted to the mailbox of {recipient_name}.\n(β•―Β°β–‘Β°)β•―πŸ“¨πŸ“¬", sender_id, interface) - # Send notification to the recipient - notification_message = f"You have a new mail message from {sender_short_name}. Check your mailbox by responding to this message with M." + notification_message = f"You have a new mail message from {sender_short_name}. Check your mailbox by responding to this message with CM." send_message(notification_message, recipient_id, interface) update_user_state(sender_id, None) @@ -398,3 +397,277 @@ def handle_channel_directory_steps(sender_id, message, step, state, interface): add_channel(channel_name, channel_url) send_message(f"Your channel '{channel_name}' has been added to the directory.", sender_id, interface) handle_channel_directory_command(sender_id, interface) + + +def handle_send_mail_command(sender_id, message, interface, bbs_nodes): + try: + parts = message.split("|", 3) + if len(parts) != 4: + send_message("Send Mail Quick Command format:\nSM|{short_name}|{subject}|{message}", sender_id, interface) + return + + _, short_name, subject, content = parts + nodes = get_node_info(interface, short_name.lower()) + if not nodes: + send_message(f"Node with short name '{short_name}' not found.", sender_id, interface) + return + if len(nodes) > 1: + send_message(f"Multiple nodes with short name '{short_name}' found. Please be more specific.", sender_id, + interface) + return + + recipient_id = nodes[0]['num'] + recipient_name = get_node_name(recipient_id, interface) + sender_short_name = get_node_short_name(get_node_id_from_num(sender_id, interface), interface) + + unique_id = add_mail(get_node_id_from_num(sender_id, interface), sender_short_name, recipient_id, subject, + content, bbs_nodes, interface) + send_message(f"Mail has been sent to {recipient_name}.", sender_id, interface) + + notification_message = f"You have a new mail message from {sender_short_name}. Check your mailbox by responding to this message with M." + send_message(notification_message, recipient_id, interface) + + except Exception as e: + logging.error(f"Error processing send mail command: {e}") + send_message("Error processing send mail command.", sender_id, interface) + + +def handle_check_mail_command(sender_id, interface): + try: + sender_node_id = get_node_id_from_num(sender_id, interface) + mail = get_mail(sender_node_id) + if not mail: + send_message("You have no new messages.", sender_id, interface) + return + + response = "πŸ“¬ You have the following messages:\n" + for i, msg in enumerate(mail): + response += f"{i + 1:02d}. From: {msg[1]}, Subject: {msg[2]}, Date: {msg[3]}\n" + response += "\nPlease reply with the number of the message you want to read." + send_message(response, sender_id, interface) + + update_user_state(sender_id, {'command': 'CHECK_MAIL', 'step': 1, 'mail': mail}) + + except Exception as e: + logging.error(f"Error processing check mail command: {e}") + send_message("Error processing check mail command.", sender_id, interface) + + +def handle_read_mail_command(sender_id, message, state, interface): + try: + mail = state.get('mail', []) + message_number = int(message) - 1 + + if message_number < 0 or message_number >= len(mail): + send_message("Invalid message number. Please try again.", sender_id, interface) + return + + mail_id = mail[message_number][0] + sender_node_id = get_node_id_from_num(sender_id, interface) + sender, date, subject, content, unique_id = get_mail_content(mail_id, sender_node_id) + response = f"Date: {date}\nFrom: {sender}\nSubject: {subject}\n\n{content}" + send_message(response, sender_id, interface) + send_message("Would you like to delete this message now that you've read it? Y/N", sender_id, interface) + update_user_state(sender_id, {'command': 'CHECK_MAIL', 'step': 2, 'mail_id': mail_id, 'unique_id': unique_id}) + + except ValueError: + send_message("Invalid input. Please enter a valid message number.", sender_id, interface) + except Exception as e: + logging.error(f"Error processing read mail command: {e}") + send_message("Error processing read mail command.", sender_id, interface) + + +def handle_delete_mail_confirmation(sender_id, message, state, interface, bbs_nodes): + try: + choice = message.lower() + if choice == 'y': + unique_id = state['unique_id'] + sender_node_id = get_node_id_from_num(sender_id, interface) + delete_mail(unique_id, sender_node_id, bbs_nodes, interface) + send_message("The message has been deleted πŸ—‘οΈ", sender_id, interface) + else: + send_message("The message has been kept in your inbox.βœ‰οΈ", sender_id, interface) + + update_user_state(sender_id, None) + + except Exception as e: + logging.error(f"Error processing delete mail confirmation: {e}") + send_message("Error processing delete mail confirmation.", sender_id, interface) + + +def handle_post_bulletin_command(sender_id, message, interface, bbs_nodes): + try: + parts = message.split("|", 3) + if len(parts) != 4: + send_message("Post Bulletin Quick Command format:\nPB|{board_name}|{subject}|{content}", sender_id, interface) + return + + _, board_name, subject, content = parts + sender_short_name = get_node_short_name(get_node_id_from_num(sender_id, interface), interface) + + unique_id = add_bulletin(board_name, sender_short_name, subject, content, bbs_nodes, interface) + send_message(f"Your bulletin '{subject}' has been posted to {board_name}.", sender_id, interface) + + # New logic to send group chat notification for urgent bulletins + if board_name.lower() == "urgent": + notification_message = f"πŸ’₯NEW URGENT BULLETINπŸ’₯\nFrom: {sender_short_name}\nTitle: {subject}" + send_message(notification_message, BROADCAST_NUM, interface) + + except Exception as e: + logging.error(f"Error processing post bulletin command: {e}") + send_message("Error processing post bulletin command.", sender_id, interface) + + +def handle_check_bulletin_command(sender_id, message, interface): + try: + parts = message.split("|", 2) + if len(parts) != 2: + send_message("Check Bulletins Quick Command format:\nCB|{board_name}", sender_id, interface) + return + + _, board_name = parts + bulletins = get_bulletins(board_name) + if not bulletins: + send_message(f"No bulletins available on {board_name} board.", sender_id, interface) + return + + response = f"πŸ“° Bulletins on {board_name} board:\n" + for i, bulletin in enumerate(bulletins): + response += f"[{i+1:02d}] Subject: {bulletin[1]}, From: {bulletin[2]}, Date: {bulletin[3]}\n" + response += "\nPlease reply with the number of the bulletin you want to read." + send_message(response, sender_id, interface) + + update_user_state(sender_id, {'command': 'CHECK_BULLETIN', 'step': 1, 'board_name': board_name, 'bulletins': bulletins}) + + except Exception as e: + logging.error(f"Error processing check bulletin command: {e}") + send_message("Error processing check bulletin command.", sender_id, interface) + +def handle_read_bulletin_command(sender_id, message, state, interface): + try: + bulletins = state.get('bulletins', []) + message_number = int(message) - 1 + + if message_number < 0 or message_number >= len(bulletins): + send_message("Invalid bulletin number. Please try again.", sender_id, interface) + return + + bulletin_id = bulletins[message_number][0] + sender, date, subject, content, unique_id = get_bulletin_content(bulletin_id) + response = f"Date: {date}\nFrom: {sender}\nSubject: {subject}\n\n{content}" + send_message(response, sender_id, interface) + + update_user_state(sender_id, None) + + except ValueError: + send_message("Invalid input. Please enter a valid bulletin number.", sender_id, interface) + except Exception as e: + logging.error(f"Error processing read bulletin command: {e}") + send_message("Error processing read bulletin command.", sender_id, interface) + + +def handle_post_channel_command(sender_id, message, interface): + try: + parts = message.split("|", 3) + if len(parts) != 3: + send_message("Post Channel Quick Command format:\nCHP|{channel_name}|{channel_url}", sender_id, interface) + return + + _, channel_name, channel_url = parts + bbs_nodes = interface.bbs_nodes + add_channel(channel_name, channel_url, bbs_nodes, interface) + send_message(f"Channel '{channel_name}' has been added to the directory.", sender_id, interface) + + except Exception as e: + logging.error(f"Error processing post channel command: {e}") + send_message("Error processing post channel command.", sender_id, interface) + + +def handle_check_channel_command(sender_id, interface): + try: + channels = get_channels() + if not channels: + send_message("No channels available in the directory.", sender_id, interface) + return + + response = "πŸ“š Available Channels:\n" + for i, channel in enumerate(channels): + response += f"{i + 1:02d}. Name: {channel[0]}\n" + response += "\nPlease reply with the number of the channel you want to view." + send_message(response, sender_id, interface) + + update_user_state(sender_id, {'command': 'CHECK_CHANNEL', 'step': 1, 'channels': channels}) + + except Exception as e: + logging.error(f"Error processing check channel command: {e}") + send_message("Error processing check channel 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_list_channels_command(sender_id, interface): + try: + channels = get_channels() + if not channels: + send_message("No channels available in the directory.", sender_id, interface) + return + + response = "πŸ“š Available Channels:\n" + for i, channel in enumerate(channels): + response += f"{i+1:02d}. Name: {channel[0]}\n" + response += "\nPlease reply with the number of the channel you want to view." + send_message(response, sender_id, interface) + + update_user_state(sender_id, {'command': 'LIST_CHANNELS', 'step': 1, 'channels': channels}) + + except Exception as e: + 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 and pipe symbol -> | to learn how to use each one\nSM| - Send " + "Mail\nCM - Check Mail (No Pipe)\nPB| - Post Bulletin\nCB| - Check Bulletins\nCHP| - Post " + "Channel\nCHL - List Channels (no Pipe)") + send_message(response, sender_id, interface) \ No newline at end of file diff --git a/db_operations.py b/db_operations.py index 0f366c7..de60722 100644 --- a/db_operations.py +++ b/db_operations.py @@ -10,7 +10,7 @@ from utils import ( send_bulletin_to_bbs_nodes, send_delete_bulletin_to_bbs_nodes, send_delete_mail_to_bbs_nodes, - send_mail_to_bbs_nodes, send_message + send_mail_to_bbs_nodes, send_message, send_channel_to_bbs_nodes ) @@ -51,12 +51,16 @@ def initialize_database(): conn.commit() print("Database schema initialized.") -def add_channel(name, url): +def add_channel(name, url, bbs_nodes=None, interface=None): conn = get_db_connection() c = conn.cursor() c.execute("INSERT INTO channels (name, url) VALUES (?, ?)", (name, url)) conn.commit() + if bbs_nodes and interface: + send_channel_to_bbs_nodes(name, url, bbs_nodes, interface) + + def get_channels(): conn = get_db_connection() c = conn.cursor() @@ -85,6 +89,7 @@ def add_bulletin(board, sender_short_name, subject, content, bbs_nodes, interfac return unique_id + def get_bulletins(board): conn = get_db_connection() c = conn.cursor() diff --git a/message_processing.py b/message_processing.py index 1efc252..766b266 100644 --- a/message_processing.py +++ b/message_processing.py @@ -6,21 +6,22 @@ from command_handlers import ( handle_mail_command, handle_bulletin_command, handle_exit_command, handle_help_command, handle_stats_command, handle_fortune_command, handle_bb_steps, handle_mail_steps, handle_stats_steps, handle_wall_of_shame_command, - handle_channel_directory_command, handle_channel_directory_steps + handle_channel_directory_command, handle_channel_directory_steps, handle_send_mail_command, + handle_read_mail_command, handle_check_mail_command, handle_delete_mail_confirmation, handle_post_bulletin_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 ) -from db_operations import add_bulletin, add_mail, delete_bulletin, delete_mail, get_db_connection +from db_operations import add_bulletin, add_mail, delete_bulletin, delete_mail, get_db_connection, add_channel from utils import get_user_state, get_node_short_name, get_node_id_from_num, send_message command_handlers = { - "m": handle_mail_command, - "b": handle_bulletin_command, - "s": handle_stats_command, - "f": handle_fortune_command, - "w": handle_wall_of_shame_command, + "qch": handle_quick_help_command, + "st": handle_stats_command, + "fo": handle_fortune_command, + "ws": handle_wall_of_shame_command, "exit": handle_exit_command, - "h": handle_help_command, - "c": handle_channel_directory_command + "help": handle_help_command } def process_message(sender_id, message, interface, is_sync_message=False): @@ -49,6 +50,10 @@ def process_message(sender_id, message, interface, is_sync_message=False): logging.info(f"Processing delete mail with unique_id: {unique_id}") recipient_id = get_recipient_id_by_mail(unique_id) # Fetch the recipient_id using this helper function delete_mail(unique_id, recipient_id, [], interface) + elif message.startswith("CHANNEL|"): + parts = message.split("|") + channel_name, channel_url = parts[1], parts[2] + add_channel(channel_name, channel_url) else: if message_lower in command_handlers: command_handlers[message_lower](sender_id, interface) @@ -64,9 +69,36 @@ def process_message(sender_id, message, interface, is_sync_message=False): handle_stats_steps(sender_id, message, step, interface, bbs_nodes) elif command == 'CHANNEL_DIRECTORY': handle_channel_directory_steps(sender_id, message, step, state, interface) + elif command == 'CHECK_MAIL': + if step == 1: + handle_read_mail_command(sender_id, message, state, interface) + elif step == 2: + handle_delete_mail_confirmation(sender_id, message, state, interface, bbs_nodes) + elif command == 'CHECK_BULLETIN': + if step == 1: + handle_read_bulletin_command(sender_id, message, state, interface) + elif command == 'CHECK_CHANNEL': + if step == 1: + handle_read_channel_command(sender_id, message, state, interface) + elif command == 'LIST_CHANNELS': + if step == 1: + handle_read_channel_command(sender_id, message, state, interface) + elif message.startswith("SM|"): + handle_send_mail_command(sender_id, message, interface, bbs_nodes) + elif message.startswith("CM"): + handle_check_mail_command(sender_id, interface) + elif message.startswith("PB|"): + handle_post_bulletin_command(sender_id, message, interface, bbs_nodes) + elif message.startswith("CB|"): + handle_check_bulletin_command(sender_id, message, interface) + elif message.startswith("CHP|"): + handle_post_channel_command(sender_id, message, interface) + elif message.startswith("CHL"): + handle_list_channels_command(sender_id, 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/utils.py b/utils.py index 4fa3009..4c4c360 100644 --- a/utils.py +++ b/utils.py @@ -71,3 +71,9 @@ def send_delete_mail_to_bbs_nodes(unique_id, bbs_nodes, interface): logging.info(f"SERVER SYNC: Sending delete mail sync message with unique_id: {unique_id}") for node_id in bbs_nodes: send_message(message, node_id, interface) + + +def send_channel_to_bbs_nodes(name, url, bbs_nodes, interface): + message = f"CHANNEL|{name}|{url}" + for node_id in bbs_nodes: + send_message(message, node_id, interface) From e5ed8bc2ab3e8b8eef9797c33718c6cc0c190585 Mon Sep 17 00:00:00 2001 From: Vince Loschiavo Date: Fri, 5 Jul 2024 15:32:36 -0700 Subject: [PATCH 13/30] Created Examples directory and added the Ferengi Rules of Acquisition --- .../example_RulesOfAcquisition_fortunes.txt | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 examples/example_RulesOfAcquisition_fortunes.txt diff --git a/examples/example_RulesOfAcquisition_fortunes.txt b/examples/example_RulesOfAcquisition_fortunes.txt new file mode 100644 index 0000000..46f44cc --- /dev/null +++ b/examples/example_RulesOfAcquisition_fortunes.txt @@ -0,0 +1,126 @@ +Ferengi Rule of Acquisition #1 "Once you have their money, you never give it back." "The Nagus" (DS9 episode) +Ferengi Rule of Acquisition #2 "The best deal is the one that makes the most profit." The 34th Rule (DS9 novel) +Ferengi Rule of Acquisition #3 "Never spend more for an acquisition than you have to." "The Maquis, Part II" (DS9 episode) +Ferengi Rule of Acquisition #4 "A woman wearing clothes is like a man in the kitchen." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #5 "Always exaggerate your estimates." Cold Fusion (SCE novel) +Ferengi Rule of Acquisition #6 "Never let family stand in the way of opportunity." "The Nagus" (DS9 episode) +Ferengi Rule of Acquisition #7 "Always keep your ears open." "In the Hands of the Prophets" (DS9 episode) +Ferengi Rule of Acquisition #8 "Small print leads to large risk." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #9 "Instinct, plus opportunity, equals profit." "The Storyteller" (DS9 episode) +Ferengi Rule of Acquisition #10 "Greed is eternal." "Prophet Motive" (VOY episode) +Ferengi Rule of Acquisition #11 "Even if its free, you can always buy it cheaper." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #12 "Anything worth selling is worth selling twice." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #13 "Anything worth doing is worth doing for money." Legends of the Ferengi (DS9 novel) +Ferengi Rule of Acquisition #15 "Dead men close no deals." Demons of Air and Darkness (DS9 novel) +Ferengi Rule of Acquisition #16 "A deal is a deal (is a deal)...until a better one comes along." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #17 "A contract is a contract is a contract... but only between Ferengi." "Body Parts" (DS9 episode) +Ferengi Rule of Acquisition #18 "A Ferengi without profit is no Ferengi at all." "Heart of Stone" (DS9 episode) +Ferengi Rule of Acquisition #19 "Satisfaction is not guaranteed." Legends of the Ferengi (DS9 novel) +Ferengi Rule of Acquisition #20 "He who dives under the table today lives to profit tomorrow." Ferenginar: Satisfaction is Not Guaranteed (DS9 novella) +Ferengi Rule of Acquisition #21 "Never place friendship before profit." "Rules of Acquisition" (DS9 episode) +Ferengi Rule of Acquisition #22 "Wise men can hear profit in the wind." "Rules of Acquisition" (DS9 episode) +Ferengi Rule of Acquisition #23 "Nothing is more important than your health...except for your money." "Acquisition" (ENT episode) +Ferengi Rule of Acquisition #25 "You can't make a deal if you're dead." "The Siege of Ar-558" (DS9 episode) +Ferengi Rule of Acquisition #27 "There's nothing more dangerous than an honest businessman." Legends of the Ferengi (DS9 novel) +Ferengi Rule of Acquisition #29 "What's in it for me?" Highest Score (DS9 novel) +Ferengi Rule of Acquisition #30 "A wise man knows that confidentiality equals profit." The Badlands, Part IV (DS9 novel) +Ferengi Rule of Acquisition #31 "Never make fun of a Ferengi's mother." "The Siege" (DS9 episode) +Ferengi Rule of Acquisition #32 "Insult something he cares about." "Elite Force II" +Ferengi Rule of Acquisition #33 "It never hurts to suck up to the boss." "Rules of Acquisition" (DS9 episode) +Ferengi Rule of Acquisition #34 "War is good for business." "Destiny" (DS9 episode); The 34th Rule (DS9 novel) +Ferengi Rule of Acquisition #35 "Peace is good for business." "Destiny" (DS9 episode); The 34th Rule (DS9 novel) +Ferengi Rule of Acquisition #37 "The early investor reaps the most interest." ST novella: Reservoir Ferengi +Ferengi Rule of Acquisition #40 "She can touch your lobes but never your latinum." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #41 "Profit is its own reward." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #43 "Feed your greed, but not enough to choke it." The Buried Age (TNG novel) +Ferengi Rule of Acquisition #44 "Never confuse wisdom with luck." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #45 "Expand or die.*" "Acquisition" (ENT episode) +Ferengi Rule of Acquisition #47 "Never trust a man wearing a better suit than your own." "Rivals" (DS9 episode) +Ferengi Rule of Acquisition #48 "The bigger the smile, the sharper the knife." "Rules of Acquisition" (DS9 episode) +Ferengi Rule of Acquisition #52 "Never ask when you can take." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #53 "Never trust anybody taller than you." Mission Gamma: Twilight (DS9 novel) +Ferengi Rule of Acquisition #54 "Rate divided by time equals profit." (Also known as "The Velocity of Wealth.") Raise the Dawn (Typhon Pact novel) +Ferengi Rule of Acquisition #55 "Take joy from profit, and profit from joy." Worlds of Deep Space Nine: Bajor: Fragments and Omens (DS9 novel) +Ferengi Rule of Acquisition #57 "Good customers are almost as rare as latinum...treasure them." "Armageddon Game" (DS9 episode) +Ferengi Rule of Acquisition #58 "There is no substitute for success." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #59 "Free advice is seldom cheap." "Rules of Acquisition" (DS9 episode) +Ferengi Rule of Acquisition #60 "Keep your lies consistent." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #62 "The riskier the road, the greater the profit." "Rules of Acquisition" (DS9 episode) +Ferengi Rule of Acquisition #63 "Work is the best therapy-at least for your employees." "Over a Torrent Sea" (TTN novel) +Ferengi Rule of Acquisition #65 "Win or lose, there's always Hupyrian beetle snuff." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #69 "Ferengi are not responsible for the stupidity of other races." Balance of Power (TNG novel) +Ferengi Rule of Acquisition #74 "Knowledge equals profit." "Inside Man" (VOY episode) +Ferengi Rule of Acquisition #75 "Home is where the heart is, but the stars are made of latinum." "Civil Defense" (DS9 episode) +Ferengi Rule of Acquisition #76 "Every once in a while, declare peace. It confuses the Hell out of your enemies." "The Homecoming" (DS9 episode) +Ferengi Rule of Acquisition #77 "If you break it, you bought it." Star Trek Online +Ferengi Rule of Acquisition #79 "Beware of the Vulcan greed for knowledge." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #82 "The flimsier the product, the higher the price." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #85 "Never let the competition know what you're thinking." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #87 "Learn the customer's weaknesses, so that you can better take advantage of him." Highest Score (DS9 novel) +Ferengi Rule of Acquisition #88 "It ain't over 'til its over." Ferenginar: Satisfaction is Not Guaranteed (DS9 novella) +Ferengi Rule of Acquisition #89 "Ask not what you can do for your profits, but what your profits can do for you." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #92 "There are many paths to profit." Highest Score (DS9 novel) +Ferengi Rule of Acquisition #94 "Females and finances don't mix." "Ferengi Love Songs" (DS9 episode) +Ferengi Rule of Acquisition #95 "Expand or die.*" "False Profits" (VOY episode) +Ferengi Rule of Acquisition #97 "Enough...is never enough." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #98 "Every man has his price." "In the Pale Moonlight" (DS9 episode) +Ferengi Rule of Acquisition #99 "Trust is the biggest liability of all." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #102 "Nature decays, but latinum lasts forever." "The Jem'Hadar" (DS9 episode) +Ferengi Rule of Acquisition #103 "Sleep can interfere with..." "Rules of Acquisition" (DS9 episode) +Ferengi Rule of Acquisition #104 "Faith moves mountains...of inventory." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #106 "There is no honor in poverty." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #109 "Dignity and an empty sack is worth the sack." "Rivals" (DS9 episode) +Ferengi Rule of Acquisition #110 "Exploitation begins at home." Star Trek Online +Ferengi Rule of Acquisition #111 "Treat people in your debt like family... exploit them." "Past Tense, Part I" (DS9 episode) +Ferengi Rule of Acquisition #112 "Never have sex with the boss' sister." "Playing God" (DS9 episode) +Ferengi Rule of Acquisition #113 "Always have sex with the boss." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #121 "Everything is for sale - even friendship." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #123 "Even a blind man can recognize the glow of Latinum." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #125 "You can't make a deal if you're dead." "The Siege of AR-558" (DS9 episode) +Ferengi Rule of Acquisition #139 "Wives serve; brother inherit." "Necessary Evil" (DS9 episode) +Ferengi Rule of Acquisition #141 "Only fools pay retail." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #144 "There's nothing wrong with charity... as long as it winds up in your pocket." Legends of the Ferengi (DS9 novel) +Ferengi Rule of Acquisition #147 "People love the bartender." Fearful Symmetry (DS9 novel) +Ferengi Rule of Acquisition #153 "Sell the sizzle, not the steak." "Deep Space Mine" (DS9 comic) +Ferengi Rule of Acquisition #162 "Even in the worst of times, someone turns a profit." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #168 "Whisper your way to success." "Treachery, Faith, and the Great River" (DS9 episode) +Ferengi Rule of Acquisition #177 "Know your enemies... but do business with them always." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #181 "Not even dishonesty can tarnish the shine of profit." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #183 "When life hands you ungaberries, make detergent." Hollow Men (DS9 novel) +Ferengi Rule of Acquisition #184 "A Ferengi waits to bid until his opponents have exhausted themselves." Balance of Power (TNG novel) +Ferengi Rule of Acquisition #188 "Not even dishonesty can tarnish the shine of profit." Star Trek Online +Ferengi Rule of Acquisition #189 "Let others keep their reputation. You keep their money." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #190 "Hear all, trust nothing." DS9 episode: "Call to Arms" +Ferengi Rule of Acquisition #192 "Never cheat a Klingon... unless you're sure you can get away with it." The Ferengi Rules of Acquisition (DS9 novel) +Ferengi Rule of Acquisition #193 "Trouble comes in threes." Star Trek Online +Ferengi Rule of Acquisition #194 "It's always good business to know about new customers before they walk in your door." "Whispers" (DS9 episode) +Ferengi Rule of Acquisition #199 "Location, location, location." The Soul Key (DS9 novel) +Ferengi Rule of Acquisition #200 "A Ferengi chooses no side but his own" (DS9 novel: Ferenginar: Satisfaction is Not Guaranteed) +Ferengi Rule of Acquisition #202 "The justification for profit is profit." The Ferengi Rules of Acquisition (DS9 reference book) +Ferengi Rule of Acquisition #203 "New customers are like razor-toothed gree worms. They can be succulent, but sometimes they bite back." DS9 episode: "Little Green Men" +Ferengi Rule of Acquisition #208 "Sometimes, the only thing more dangerous than a question is an answer." DS9 episode: "Ferengi Love Songs" +Ferengi Rule of Acquisition #211 "Employees are the rungs on the ladder of success, don't hesitate to step on them." DS9 episode: "Bar Association" +Ferengi Rule of Acquisition #212 "A good lie is easier to believe than the truth." Star Trek Online +Ferengi Rule of Acquisition #214 "Never begin a business transaction on an empty stomach." DS9 episode: "The Maquis, Part I" +Ferengi Rule of Acquisition #216 "Never gamble with a telepath." DS9 novel: The Laertian Gamble +Ferengi Rule of Acquisition #217 "You can't free a fish from water." DS9 episode: "Past Tense, Part I" +Ferengi Rule of Acquisition #218 "Always know what you're buying." The Ferengi Rules of Acquisition (DS9 reference book) +Ferengi Rule of Acquisition #218 "Sometimes what you get free cost entirely too much." Baby on Board (DS9 Malibu Comics) +Ferengi Rule of Acquisition #219 "Possession is eleven-tenths of the law!" TNG novel: Balance of Power +Ferengi Rule of Acquisition #223 "Beware the man who doesn't take time for Oo-mox." The Ferengi Rules of Acquisition (DS9 reference book) +Ferengi Rule of Acquisition #227 "If that's what's written, then that's what's written." Star Trek Online +Ferengi Rule of Acquisition #229 "Latinum lasts longer than lust." DS9 episode: "Ferengi Love Songs" +Ferengi Rule of Acquisition #235 "Duck; death is tall." Mission Gamma: Twilight (DS9 novel) +Ferengi Rule of Acquisition #236 "You can't buy fate." The Ferengi Rules of Acquisition (DS9 reference book) +Ferengi Rule of Acquisition #239 "Never be afraid to mislabel a product." DS9 episode: "Body Parts" +Ferengi Rule of Acquisition #240 "Time, like latinum, is a highly limited commodity." Star Trek Online +Ferengi Rule of Acquisition #242 "More is good...all is better." The Ferengi Rules of Acquisition (DS9 reference book) +Ferengi Rule of Acquisition #255 "A wife is [a] luxury... a smart accountant a neccessity." The Ferengi Rules of Acquisition (DS9 reference book) +Ferengi Rule of Acquisition #257 "When the messenger comes to appropriate your profits, kill the messenger." False Profits (Voyager) +Ferengi Rule of Acquisition #261 "A wealthy man can afford everything except a conscience." The Ferengi Rules of Acquisition (DS9 reference book) +Ferengi Rule of Acquisition #263 "Never let doubt interfere with your lust for Latinum." DS9 episode: "Bar Association" +Ferengi Rule of Acquisition #266 "When in doubt, lie." The Ferengi Rules of Acquisition (DS9 reference book) +Ferengi Rule of Acquisition #272 "Always inspect the merchandise before making a deal." Star Trek Online +Ferengi Rule of Acquisition #280 "If it ain't broke, don't fix it." DS9 novel: Ferenginar: Satisfaction is Not Guaranteed) +Ferengi Rule of Acquisition #284 "Deep down, everyone's a Ferengi." The Ferengi Rules of Acquisition (DS9 reference book) +Ferengi Rule of Acquisition #285 "No good deed ever goes unpunished." DS9 episode: "The Collaborator" From c0923576fcbbda8a66fd22ce3a31283a6c846703 Mon Sep 17 00:00:00 2001 From: Vince Loschiavo Date: Fri, 5 Jul 2024 15:46:07 -0700 Subject: [PATCH 14/30] moved example files to their own directory; renamed fortunes.txt to example_fortunes.txt so as not to overwrite a user's custom fortunes.txt file. --- fortunes.txt => example_fortunes.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename fortunes.txt => example_fortunes.txt (100%) diff --git a/fortunes.txt b/example_fortunes.txt similarity index 100% rename from fortunes.txt rename to example_fortunes.txt From ab154b2934d754a34d1bb75cb309d7d139535ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Tue, 9 Jul 2024 11:57:38 -0400 Subject: [PATCH 15/30] Menu Structure Changes Menu structure changes to improve performance --- command_handlers.py | 235 ++++++++++++++++-------------------------- example_config.ini | 34 ------ message_processing.py | 157 +++++++++++++++++++--------- server.py | 4 +- 4 files changed, 198 insertions(+), 232 deletions(-) delete mode 100644 example_config.ini diff --git a/command_handlers.py b/command_handlers.py index 6cd22fe..9e96746 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -4,7 +4,6 @@ import time from meshtastic import BROADCAST_NUM -from config_init import initialize_config from db_operations import ( add_bulletin, add_mail, delete_mail, get_bulletin_content, get_bulletins, @@ -18,6 +17,21 @@ from utils import ( ) +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" + elif menu_name == 'utilities': + response = "πŸ› οΈUtilities MenuπŸ› οΈ\n[S]tats\n[F]ortune\n[W]all of Shame\nE[X]IT" + else: + update_user_state(sender_id, {'command': 'MAIN_MENU', 'step': 1}) # Reset to main menu state + response = "πŸ’ΎTCΒ² BBSπŸ’Ύ\n[Q]uick Commands\n[B]BS\n[U]tilities\nE[X]IT" + send_message(response, sender_id, interface) + + + + def get_node_name(node_id, interface): node_info = interface.nodes.get(node_id) if node_info: @@ -26,15 +40,16 @@ def get_node_name(node_id, interface): def handle_mail_command(sender_id, interface): - response = "βœ‰οΈ MAIL MENU βœ‰οΈ\nWhat would you like to do with mail?\n[0]Read [1]Send [2]Exit" + response = "βœ‰οΈMail Menuβœ‰οΈ\nWhat would you like to do with mail?\n[R]ead [S]end E[X]IT" send_message(response, sender_id, interface) update_user_state(sender_id, {'command': 'MAIL', 'step': 1}) + def handle_bulletin_command(sender_id, interface): - response = "πŸ“° BULLETIN MENU πŸ“°\nWhich board would you like to enter?\n[0]General [1]Info [2]News [3]Urgent [4]Exit" + response = "πŸ“°Bulletin MenuπŸ“°\nWhich board would you like to enter?\n[G]eneral [I]nfo [N]ews [U]rgent" send_message(response, sender_id, interface) - update_user_state(sender_id, {'command': 'BULLETIN', 'step': 1}) + update_user_state(sender_id, {'command': 'BULLETIN_MENU', 'step': 1}) def handle_exit_command(sender_id, interface): @@ -42,45 +57,8 @@ def handle_exit_command(sender_id, interface): update_user_state(sender_id, None) -def handle_help_command(sender_id, interface, state=None): - title = "πŸ’ΎTC2 BBSπŸ’Ύ\n" - commands = [ - "[QCH] - Quick Commands", - "[St]ats Menu", - "[Fo]rtune", - "[WS]Wall of Shame", - "[EXIT]", - "[HELP]" - ] - if state and 'command' in state: - current_command = state['command'] - if current_command == 'MAIL': - commands = [ - "[0]Read Mail", - "[1]Send Mail", - "[2]Exit Mail Menu" - ] - elif current_command == 'BULLETIN': - commands = [ - "[0]General Board", - "[1]Info Board", - "[2]News Board", - "[3]Urgent Board", - "[4]Exit Bulletin Menu" - ] - elif current_command == 'STATS': - commands = [ - "[0]Total Nodes", - "[1]Total HW Models", - "[2]Total Roles", - "[3]Back" - ] - response = title + "Available commands:\n" + "\n".join(commands) - send_message(response, sender_id, interface) - - def handle_stats_command(sender_id, interface): - response = "What stats would you like to view?\n[0]Node Numbers [1]Hardware [2]Roles [3]Main Menu" + response = "πŸ“ŠStats MenuπŸ“Š\nWhat stats would you like to view?\n[N]odes [H]ardware [R]oles E[X]IT" send_message(response, sender_id, interface) update_user_state(sender_id, {'command': 'STATS', 'step': 1}) @@ -99,18 +77,34 @@ def handle_fortune_command(sender_id, interface): send_message(f"Error generating fortune: {e}", sender_id, interface) -def handle_stats_steps(sender_id, message, step, interface, bbs_nodes): +def handle_stats_steps(sender_id, message, step, interface): if step == 1: - choice = message.upper() - if choice == '3': + choice = message.lower() + if choice == 'x': handle_help_command(sender_id, interface) return - choice = int(choice) - if choice == 0: - response = "Select time period for total nodes:\n[0]ALL [1]Last 24 Hours [2]Last 8 Hours [3]Last Hour" + elif choice == 'n': + current_time = int(time.time()) + timeframes = { + "All time": None, + "Last 24 hours": 86400, + "Last 8 hours": 28800, + "Last hour": 3600 + } + total_nodes_summary = [] + + for period, seconds in timeframes.items(): + if seconds is None: + total_nodes = len(interface.nodes) + else: + time_limit = current_time - seconds + total_nodes = sum(1 for node in interface.nodes.values() if node.get('lastHeard', 0) >= time_limit) + total_nodes_summary.append(f"- {period}: {total_nodes}") + + response = "Total nodes seen:\n" + "\n".join(total_nodes_summary) send_message(response, sender_id, interface) - update_user_state(sender_id, {'command': 'STATS', 'step': 2}) - elif choice == 1: + handle_stats_command(sender_id, interface) + elif choice == 'h': hw_models = {} for node in interface.nodes.values(): hw_model = node['user'].get('hwModel', 'Unknown') @@ -118,7 +112,7 @@ def handle_stats_steps(sender_id, message, step, interface, bbs_nodes): response = "Hardware Models:\n" + "\n".join([f"{model}: {count}" for model, count in hw_models.items()]) send_message(response, sender_id, interface) handle_stats_command(sender_id, interface) - elif choice == 2: + elif choice == 'r': roles = {} for node in interface.nodes.values(): role = node['user'].get('role', 'Unknown') @@ -127,94 +121,48 @@ def handle_stats_steps(sender_id, message, step, interface, bbs_nodes): send_message(response, sender_id, interface) handle_stats_command(sender_id, interface) - elif step == 2: - choice = int(message) - current_time = int(time.time()) - if choice == 0: - total_nodes = len(interface.nodes) - send_message(f"Total nodes seen: {total_nodes}", sender_id, interface) - else: - time_limits = [86400, 28800, 3600] # Last 24 hours, Last 8 hours, Last hour - time_limit = current_time - time_limits[choice - 1] - total_nodes = 0 - for node in interface.nodes.values(): - last_heard = node.get('lastHeard', 0) - if last_heard is not None and last_heard >= time_limit: - total_nodes += 1 - logging.info(f"Node {node.get('user', {}).get('longName', 'Unknown')} heard at {last_heard}, within limit {time_limit}") - timeframes = ["24 hours", "8 hours", "hour"] - send_message(f"Total nodes seen in the last {timeframes[choice - 1]}: {total_nodes}", sender_id, interface) - handle_stats_steps(sender_id, '0', 1, interface, bbs_nodes) def handle_bb_steps(sender_id, message, step, state, interface, bbs_nodes): boards = {0: "General", 1: "Info", 2: "News", 3: "Urgent"} - if step == 1: - if message == '4': - handle_help_command(sender_id, interface) + if message.lower() == 'e': + handle_help_command(sender_id, interface, 'bbs') return - board_name = boards.get(int(message)) - if board_name: - response = f"What would you like to do in the {board_name} board?\n[0]View Bulletins [1]Post Bulletin [2]Exit" - send_message(response, sender_id, interface) - update_user_state(sender_id, {'command': 'BULLETIN', 'step': 2, 'board': board_name}) - else: - handle_help_command(sender_id, interface) - update_user_state(sender_id, None) + board_name = state['board'] + response = f"What would you like to do in the {board_name} board?\n[R]ead [P]ost" + send_message(response, sender_id, interface) + update_user_state(sender_id, {'command': 'BULLETIN_ACTION', 'step': 2, 'board': board_name}) elif step == 2: - if message == '2': - # Return to the bulletin menu - response = "πŸ“° BULLETIN MENU πŸ“°\nWhich board would you like to enter?\n[0]General [1]Info [2]News [3]Urgent [4]Exit" - send_message(response, sender_id, interface) - update_user_state(sender_id, {'command': 'BULLETIN', 'step': 1}) - return - if message == '0': - board_name = state['board'] + board_name = state['board'] + if message.lower() == 'r': bulletins = get_bulletins(board_name) - if (bulletins): + if bulletins: send_message(f"Select a bulletin number to view from {board_name}:", sender_id, interface) for bulletin in bulletins: send_message(f"[{bulletin[0]}] {bulletin[1]}", sender_id, interface) - update_user_state(sender_id, {'command': 'BULLETIN', 'step': 3, 'board': board_name}) + update_user_state(sender_id, {'command': 'BULLETIN_READ', 'step': 3, 'board': board_name}) else: send_message(f"No bulletins in {board_name}.", sender_id, interface) - # Go back to the board menu - response = f"What would you like to do in the {board_name} board?\n[0]View Bulletins [1]Post Bulletin [2]Exit" - send_message(response, sender_id, interface) - update_user_state(sender_id, {'command': 'BULLETIN', 'step': 2, 'board': board_name}) - - elif message == '1': + handle_bb_steps(sender_id, 'e', 1, state, interface, bbs_nodes) + elif message.lower() == 'p': send_message("What is the subject of your bulletin? Keep it short.", sender_id, interface) - update_user_state(sender_id, {'command': 'BULLETIN', 'step': 4, 'board': state['board']}) + update_user_state(sender_id, {'command': 'BULLETIN_POST', 'step': 4, 'board': board_name}) elif step == 3: bulletin_id = int(message) sender_short_name, date, subject, content, unique_id = get_bulletin_content(bulletin_id) send_message(f"From: {sender_short_name}\nDate: {date}\nSubject: {subject}\n- - - - - - -\n{content}", sender_id, interface) board_name = state['board'] - response = f"What would you like to do in the {board_name} board?\n[0]View Bulletins [1]Post Bulletin [2]Exit" - send_message(response, sender_id, interface) - update_user_state(sender_id, {'command': 'BULLETIN', 'step': 2, 'board': board_name}) + handle_bb_steps(sender_id, 'e', 1, state, interface, bbs_nodes) elif step == 4: subject = message send_message("Send the contents of your bulletin. Send a message with END when finished.", sender_id, interface) - update_user_state(sender_id, {'command': 'BULLETIN', 'step': 6, 'board': state['board'], 'subject': subject, 'content': ''}) + update_user_state(sender_id, {'command': 'BULLETIN_POST_CONTENT', 'step': 5, 'board': state['board'], 'subject': subject, 'content': ''}) elif step == 5: - if message.lower() == "y": - bulletins = get_bulletins(state['board']) - send_message(f"Select a bulletin number to view from {state['board']}:", sender_id, interface) - for bulletin in bulletins: - send_message(f"[{bulletin[0]}]\nSubject: {bulletin[1]}", sender_id, interface) - update_user_state(sender_id, {'command': 'BULLETIN', 'step': 3, 'board': state['board']}) - else: - send_message("Okay, feel free to send another command.", sender_id, interface) - update_user_state(sender_id, None) - - elif step == 6: if message.lower() == "end": board = state['board'] subject = state['subject'] @@ -228,48 +176,44 @@ def handle_bb_steps(sender_id, message, step, state, interface, bbs_nodes): sender_short_name = node_info['user'].get('shortName', f"Node {sender_id}") unique_id = add_bulletin(board, sender_short_name, subject, content, bbs_nodes, interface) send_message(f"Your bulletin '{subject}' has been posted to {board}.\n(β•―Β°β–‘Β°)β•―πŸ“„πŸ“Œ[{board}]", sender_id, interface) - response = f"What would you like to do in the {board} board?\n[0]View Bulletins [1]Post Bulletin [2]Exit" - send_message(response, sender_id, interface) - update_user_state(sender_id, {'command': 'BULLETIN', 'step': 2, 'board': board}) + handle_bb_steps(sender_id, 'e', 1, state, interface, bbs_nodes) else: state['content'] += message + "\n" update_user_state(sender_id, state) + def handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes): if step == 1: - choice = message - if choice == '0': + choice = message.lower() + if choice == 'r': sender_node_id = get_node_id_from_num(sender_id, interface) mail = get_mail(sender_node_id) if mail: send_message(f"You have {len(mail)} mail messages. Select a message number to read:", sender_id, interface) for msg in mail: - send_message(f"βœ‰οΈ {msg[0]} βœ‰οΈ\nDate: {msg[3]}\nFrom: {msg[1]}\nSubject: {msg[2]}", sender_id, interface) + send_message(f"-{msg[0]}-\nDate: {msg[3]}\nFrom: {msg[1]}\nSubject: {msg[2]}", sender_id, interface) update_user_state(sender_id, {'command': 'MAIL', 'step': 2}) else: - send_message("There are no messages in your mailbox.\n(`βŒ’`)", sender_id, interface) + send_message("There are no messages in your mailbox.πŸ“­", sender_id, interface) update_user_state(sender_id, None) - elif choice == '1': + elif choice == 's': send_message("What is the Short Name of the node you want to leave a message for?", sender_id, interface) update_user_state(sender_id, {'command': 'MAIL', 'step': 3}) - elif choice == '2': + elif choice == 'x': handle_help_command(sender_id, interface) elif step == 2: mail_id = int(message) try: - - # ERROR: sender_id is not what is stored in the DB sender_node_id = get_node_id_from_num(sender_id, interface) sender, date, subject, content, unique_id = get_mail_content(mail_id, sender_node_id) send_message(f"Date: {date}\nFrom: {sender}\nSubject: {subject}\n{content}", sender_id, interface) send_message("Would you like to delete this message now that you've viewed it? Y/N", sender_id, interface) update_user_state(sender_id, {'command': 'MAIL', 'step': 4, 'mail_id': mail_id, 'unique_id': unique_id}) except TypeError: - # get_main_content returned None. Node tried to access somebody's else mail message logging.info(f"Node {sender_id} tried to access non-existent message") - send_message(f"Mail not found", sender_id, interface) + send_message("Mail not found", sender_id, interface) update_user_state(sender_id, None) elif step == 3: @@ -296,7 +240,7 @@ def handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes): delete_mail(unique_id, sender_node_id, bbs_nodes, interface) send_message("The message has been deleted πŸ—‘οΈ", sender_id, interface) else: - send_message("The message has been kept in your inbox.βœ‰οΈ\nJust don't let it get as messy as your regular email inbox (ΰ² _ΰ² )", sender_id, interface) + send_message("The message has been kept in your inbox.βœ‰οΈ", sender_id, interface) update_user_state(sender_id, None) elif step == 5: @@ -339,6 +283,7 @@ def handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes): update_user_state(sender_id, None) + def handle_wall_of_shame_command(sender_id, interface): response = "Devices with battery levels below 20%:\n" for node_id, node in interface.nodes.items(): @@ -353,18 +298,18 @@ def handle_wall_of_shame_command(sender_id, interface): def handle_channel_directory_command(sender_id, interface): - response = "πŸ“š CHANNEL DIRECTORY πŸ“š\nWhat would you like to do in the Channel Directory?\n[0]View [1]Post [2]Exit" + response = "πŸ“šCHANNEL DIRECTORYπŸ“š\nWhat would you like to do?\n[V]iew [P]ost E[X]IT" send_message(response, sender_id, interface) update_user_state(sender_id, {'command': 'CHANNEL_DIRECTORY', 'step': 1}) def handle_channel_directory_steps(sender_id, message, step, state, interface): if step == 1: - choice = message - if choice == '2': + choice = message.lower() + if choice == 'x': handle_help_command(sender_id, interface) return - elif choice == '0': + elif choice == 'v': channels = get_channels() if channels: response = "Select a channel number to view:\n" + "\n".join( @@ -374,7 +319,7 @@ def handle_channel_directory_steps(sender_id, message, step, state, interface): else: send_message("No channels available in the directory.", sender_id, interface) handle_channel_directory_command(sender_id, interface) - elif choice == '1': + elif choice == 'p': send_message("Name your channel for the directory:", sender_id, interface) update_user_state(sender_id, {'command': 'CHANNEL_DIRECTORY', 'step': 3}) @@ -401,9 +346,9 @@ def handle_channel_directory_steps(sender_id, message, step, state, interface): def handle_send_mail_command(sender_id, message, interface, bbs_nodes): try: - parts = message.split("|", 3) + parts = message.split(",,", 3) if len(parts) != 4: - send_message("Send Mail Quick Command format:\nSM|{short_name}|{subject}|{message}", sender_id, interface) + send_message("Send Mail Quick Command format:\nSM,,{short_name},,{subject},,{message}", sender_id, interface) return _, short_name, subject, content = parts @@ -424,7 +369,7 @@ def handle_send_mail_command(sender_id, message, interface, bbs_nodes): content, bbs_nodes, interface) send_message(f"Mail has been sent to {recipient_name}.", sender_id, interface) - notification_message = f"You have a new mail message from {sender_short_name}. Check your mailbox by responding to this message with M." + notification_message = f"You have a new mail message from {sender_short_name}. Check your mailbox by responding to this message with CM." send_message(notification_message, recipient_id, interface) except Exception as e: @@ -442,7 +387,7 @@ def handle_check_mail_command(sender_id, interface): response = "πŸ“¬ You have the following messages:\n" for i, msg in enumerate(mail): - response += f"{i + 1:02d}. From: {msg[1]}, Subject: {msg[2]}, Date: {msg[3]}\n" + response += f"{i + 1:02d}. From: {msg[1]}, Subject: {msg[2]}\n" response += "\nPlease reply with the number of the message you want to read." send_message(response, sender_id, interface) @@ -497,9 +442,9 @@ def handle_delete_mail_confirmation(sender_id, message, state, interface, bbs_no def handle_post_bulletin_command(sender_id, message, interface, bbs_nodes): try: - parts = message.split("|", 3) + parts = message.split(",,", 3) if len(parts) != 4: - send_message("Post Bulletin Quick Command format:\nPB|{board_name}|{subject}|{content}", sender_id, interface) + send_message("Post Bulletin Quick Command format:\nPB,,{board_name},,{subject},,{content}", sender_id, interface) return _, board_name, subject, content = parts @@ -508,7 +453,6 @@ def handle_post_bulletin_command(sender_id, message, interface, bbs_nodes): unique_id = add_bulletin(board_name, sender_short_name, subject, content, bbs_nodes, interface) send_message(f"Your bulletin '{subject}' has been posted to {board_name}.", sender_id, interface) - # New logic to send group chat notification for urgent bulletins if board_name.lower() == "urgent": notification_message = f"πŸ’₯NEW URGENT BULLETINπŸ’₯\nFrom: {sender_short_name}\nTitle: {subject}" send_message(notification_message, BROADCAST_NUM, interface) @@ -520,9 +464,9 @@ def handle_post_bulletin_command(sender_id, message, interface, bbs_nodes): def handle_check_bulletin_command(sender_id, message, interface): try: - parts = message.split("|", 2) + parts = message.split(",,", 2) if len(parts) != 2: - send_message("Check Bulletins Quick Command format:\nCB|{board_name}", sender_id, interface) + send_message("Check Bulletins Quick Command format:\nCB,,{board_name}", sender_id, interface) return _, board_name = parts @@ -570,7 +514,7 @@ def handle_post_channel_command(sender_id, message, interface): try: parts = message.split("|", 3) if len(parts) != 3: - send_message("Post Channel Quick Command format:\nCHP|{channel_name}|{channel_url}", sender_id, interface) + send_message("Post Channel Quick Command format:\nCHP,,{channel_name},,{channel_url}", sender_id, interface) return _, channel_name, channel_url = parts @@ -590,7 +534,7 @@ def handle_check_channel_command(sender_id, interface): send_message("No channels available in the directory.", sender_id, interface) return - response = "πŸ“š Available Channels:\n" + response = "Available Channels:\n" for i, channel in enumerate(channels): response += f"{i + 1:02d}. Name: {channel[0]}\n" response += "\nPlease reply with the number of the channel you want to view." @@ -632,7 +576,7 @@ def handle_list_channels_command(sender_id, interface): send_message("No channels available in the directory.", sender_id, interface) return - response = "πŸ“š Available Channels:\n" + response = "Available Channels:\n" for i, channel in enumerate(channels): response += f"{i+1:02d}. Name: {channel[0]}\n" response += "\nPlease reply with the number of the channel you want to view." @@ -667,7 +611,6 @@ def handle_read_channel_command(sender_id, message, state, interface): def handle_quick_help_command(sender_id, interface): - response = ("πŸƒQUICK COMMANDSπŸƒβ€βž‘οΈ\nSend command and pipe symbol -> | to learn how to use each one\nSM| - Send " - "Mail\nCM - Check Mail (No Pipe)\nPB| - Post Bulletin\nCB| - Check Bulletins\nCHP| - Post " - "Channel\nCHL - List Channels (no Pipe)") + 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 diff --git a/example_config.ini b/example_config.ini deleted file mode 100644 index b953fd1..0000000 --- a/example_config.ini +++ /dev/null @@ -1,34 +0,0 @@ -############################### -#### Select Interface type #### -############################### -# [type = serial] for USB connected devices -#If there are multiple serial devices connected, be sure to use the "port" option and specify a port -# Linux Example: -# port = /dev/ttyUSB0 -# -# Windows Example: -# port = COM3 -# [type = tcp] for network connected devices (ESP32 devices only - this does not work for WisBlock) -# If using tcp, remove the # from the beginning and replace 192.168.x.x with the IP address of your device -# Example: -# [interface] -# type = tcp -# hostname = 192.168.1.100 - -[interface] -type = serial -# port = /dev/ttyACM0 -# hostname = 192.168.x.x - - -############################ -#### BBS NODE SYNC LIST #### -############################ -# Provide a list of other nodes running TCΒ²-BBS to sync mail messages and bulletins with -# Enter in a list of other BBS Nodes by their nodeID separated by commas (no spaces) -# Example: -# [sync] -# bbs_nodes = !17d7e4b7,!18e9f5a3,!1a2b3c4d - -# [sync] -# bbs_nodes = !17d7e4b7 diff --git a/message_processing.py b/message_processing.py index 766b266..03bc56f 100644 --- a/message_processing.py +++ b/message_processing.py @@ -3,25 +3,52 @@ import logging from meshtastic import BROADCAST_NUM from command_handlers import ( - handle_mail_command, handle_bulletin_command, handle_exit_command, - handle_help_command, handle_stats_command, handle_fortune_command, + handle_mail_command, handle_bulletin_command, handle_help_command, handle_stats_command, handle_fortune_command, handle_bb_steps, handle_mail_steps, handle_stats_steps, handle_wall_of_shame_command, handle_channel_directory_command, handle_channel_directory_steps, handle_send_mail_command, handle_read_mail_command, handle_check_mail_command, handle_delete_mail_confirmation, handle_post_bulletin_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 ) - from db_operations import add_bulletin, add_mail, delete_bulletin, delete_mail, get_db_connection, add_channel from utils import get_user_state, get_node_short_name, get_node_id_from_num, send_message -command_handlers = { - "qch": handle_quick_help_command, - "st": handle_stats_command, - "fo": handle_fortune_command, - "ws": handle_wall_of_shame_command, - "exit": handle_exit_command, - "help": handle_help_command +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'), + "x": handle_help_command +} + +bbs_menu_handlers = { + "m": handle_mail_command, + "b": handle_bulletin_command, + "c": handle_channel_directory_command, + "x": handle_help_command +} + + +utilities_menu_handlers = { + "s": handle_stats_command, + "f": handle_fortune_command, + "w": handle_wall_of_shame_command, + "x": handle_help_command +} + + +bulletin_menu_handlers = { + "g": lambda sender_id, interface: handle_bb_steps(sender_id, '0', 1, {'board': 'General'}, interface, None), + "i": lambda sender_id, interface: handle_bb_steps(sender_id, '1', 1, {'board': 'Info'}, interface, None), + "n": lambda sender_id, interface: handle_bb_steps(sender_id, '2', 1, {'board': 'News'}, interface, None), + "u": lambda sender_id, interface: handle_bb_steps(sender_id, '3', 1, {'board': 'Urgent'}, interface, None), + "x": handle_help_command +} + + +board_action_handlers = { + "r": lambda sender_id, interface, state: handle_bb_steps(sender_id, 'r', 2, state, interface, None), + "p": lambda sender_id, interface, state: handle_bb_steps(sender_id, 'p', 2, state, interface, None), + "x": handle_help_command } def process_message(sender_id, message, interface, is_sync_message=False): @@ -48,55 +75,85 @@ def process_message(sender_id, message, interface, is_sync_message=False): elif message.startswith("DELETE_MAIL|"): unique_id = message.split("|")[1] logging.info(f"Processing delete mail with unique_id: {unique_id}") - recipient_id = get_recipient_id_by_mail(unique_id) # Fetch the recipient_id using this helper function + recipient_id = get_recipient_id_by_mail(unique_id) delete_mail(unique_id, recipient_id, [], interface) elif message.startswith("CHANNEL|"): parts = message.split("|") channel_name, channel_url = parts[1], parts[2] add_channel(channel_name, channel_url) else: - if message_lower in command_handlers: - command_handlers[message_lower](sender_id, interface) - elif state: - command = state['command'] - step = state['step'] - - if command == 'MAIL': - handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes) - elif command == 'BULLETIN': - handle_bb_steps(sender_id, message, step, state, interface, bbs_nodes) - elif command == 'STATS': - handle_stats_steps(sender_id, message, step, interface, bbs_nodes) - elif command == 'CHANNEL_DIRECTORY': - handle_channel_directory_steps(sender_id, message, step, state, interface) - elif command == 'CHECK_MAIL': - if step == 1: - handle_read_mail_command(sender_id, message, state, interface) - elif step == 2: - handle_delete_mail_confirmation(sender_id, message, state, interface, bbs_nodes) - elif command == 'CHECK_BULLETIN': - if step == 1: - handle_read_bulletin_command(sender_id, message, state, interface) - elif command == 'CHECK_CHANNEL': - if step == 1: - handle_read_channel_command(sender_id, message, state, interface) - elif command == 'LIST_CHANNELS': - if step == 1: - handle_read_channel_command(sender_id, message, state, interface) - elif message.startswith("SM|"): - handle_send_mail_command(sender_id, message, interface, bbs_nodes) - elif message.startswith("CM"): + if message_lower.startswith("sm,,"): + handle_send_mail_command(sender_id, message_lower, interface, bbs_nodes) + elif message_lower.startswith("cm"): handle_check_mail_command(sender_id, interface) - elif message.startswith("PB|"): - handle_post_bulletin_command(sender_id, message, interface, bbs_nodes) - elif message.startswith("CB|"): - handle_check_bulletin_command(sender_id, message, interface) - elif message.startswith("CHP|"): - handle_post_channel_command(sender_id, message, interface) - elif message.startswith("CHL"): + elif message_lower.startswith("pb,,"): + handle_post_bulletin_command(sender_id, message_lower, interface, bbs_nodes) + elif message_lower.startswith("cb,,"): + handle_check_bulletin_command(sender_id, message_lower, interface) + elif message_lower.startswith("chp,,"): + handle_post_channel_command(sender_id, message_lower, interface) + elif message_lower.startswith("chl"): handle_list_channels_command(sender_id, interface) else: - handle_help_command(sender_id, interface) + if state and state['command'] == 'MENU': + menu_name = state['menu'] + if menu_name == 'bbs': + handlers = bbs_menu_handlers + elif menu_name == 'utilities': + handlers = utilities_menu_handlers + else: + handlers = main_menu_handlers + elif state and state['command'] == 'BULLETIN_MENU': + handlers = bulletin_menu_handlers + elif state and state['command'] == 'BULLETIN_ACTION': + handlers = board_action_handlers + else: + handlers = main_menu_handlers + + if message_lower == 'x': + # Reset to main menu state + handle_help_command(sender_id, interface) + return + + if message_lower in handlers: + if state and state['command'] in ['BULLETIN_ACTION', 'BULLETIN_READ', 'BULLETIN_POST', 'BULLETIN_POST_CONTENT']: + handlers[message_lower](sender_id, interface, state) + else: + handlers[message_lower](sender_id, interface) + elif state: + command = state['command'] + step = state['step'] + + if command == 'MAIL': + handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes) + elif command == 'BULLETIN': + handle_bb_steps(sender_id, message, step, state, interface, bbs_nodes) + elif command == 'STATS': + handle_stats_steps(sender_id, message, step, interface) + elif command == 'CHANNEL_DIRECTORY': + handle_channel_directory_steps(sender_id, message, step, state, interface) + elif command == 'CHECK_MAIL': + if step == 1: + handle_read_mail_command(sender_id, message, state, interface) + elif step == 2: + handle_delete_mail_confirmation(sender_id, message, state, interface, bbs_nodes) + elif command == 'CHECK_BULLETIN': + if step == 1: + handle_read_bulletin_command(sender_id, message, state, interface) + elif command == 'CHECK_CHANNEL': + if step == 1: + handle_read_channel_command(sender_id, message, state, interface) + elif command == 'LIST_CHANNELS': + if step == 1: + handle_read_channel_command(sender_id, message, state, interface) + elif command == 'BULLETIN_POST': + handle_bb_steps(sender_id, message, 4, state, interface, bbs_nodes) + elif command == 'BULLETIN_POST_CONTENT': + 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) + else: + handle_help_command(sender_id, interface) def on_receive(packet, interface): diff --git a/server.py b/server.py index af5117c..a31511a 100644 --- a/server.py +++ b/server.py @@ -2,8 +2,8 @@ """ TCΒ²-BBS Server for Meshtastic by TheCommsChannel (TCΒ²) -Date: 06/25/2024 -Version: 0.1.0 +Date: 07/09/2024 +Version: 0.1.4 Description: The system allows for mail message handling, bulletin boards, and a channel From 7f6512247718ca6741a0ead3645649ddd78dce8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:14:30 -0400 Subject: [PATCH 16/30] Create example_config.ini --- example_config.ini | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 example_config.ini diff --git a/example_config.ini b/example_config.ini new file mode 100644 index 0000000..b953fd1 --- /dev/null +++ b/example_config.ini @@ -0,0 +1,34 @@ +############################### +#### Select Interface type #### +############################### +# [type = serial] for USB connected devices +#If there are multiple serial devices connected, be sure to use the "port" option and specify a port +# Linux Example: +# port = /dev/ttyUSB0 +# +# Windows Example: +# port = COM3 +# [type = tcp] for network connected devices (ESP32 devices only - this does not work for WisBlock) +# If using tcp, remove the # from the beginning and replace 192.168.x.x with the IP address of your device +# Example: +# [interface] +# type = tcp +# hostname = 192.168.1.100 + +[interface] +type = serial +# port = /dev/ttyACM0 +# hostname = 192.168.x.x + + +############################ +#### BBS NODE SYNC LIST #### +############################ +# Provide a list of other nodes running TCΒ²-BBS to sync mail messages and bulletins with +# Enter in a list of other BBS Nodes by their nodeID separated by commas (no spaces) +# Example: +# [sync] +# bbs_nodes = !17d7e4b7,!18e9f5a3,!1a2b3c4d + +# [sync] +# bbs_nodes = !17d7e4b7 From be5a9d8c510a74ae59c7ce35a8c9a3e64e12c7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:15:52 -0400 Subject: [PATCH 17/30] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index af2f5e3..2b8a1e0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ venv/ .venv .idea config.ini +fortunes.txt From ed90448164d9ee05149da04af72bd462769a4d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:05:33 -0400 Subject: [PATCH 18/30] Urgent board permissions This adds a feature for Urgent board permissions where you can limit who can post to the urgent board via a list in config.ini --- command_handlers.py | 9 +++++++-- config_init.py | 30 +++++++++++++++++++++++------- server.py | 8 +++----- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/command_handlers.py b/command_handlers.py index 9e96746..49df599 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -129,7 +129,7 @@ def handle_bb_steps(sender_id, message, step, state, interface, bbs_nodes): if message.lower() == 'e': handle_help_command(sender_id, interface, 'bbs') return - board_name = state['board'] + board_name = boards[int(message)] response = f"What would you like to do in the {board_name} board?\n[R]ead [P]ost" send_message(response, sender_id, interface) update_user_state(sender_id, {'command': 'BULLETIN_ACTION', 'step': 2, 'board': board_name}) @@ -147,6 +147,12 @@ def handle_bb_steps(sender_id, message, step, state, interface, bbs_nodes): send_message(f"No bulletins in {board_name}.", sender_id, interface) handle_bb_steps(sender_id, 'e', 1, state, interface, bbs_nodes) elif message.lower() == 'p': + if board_name.lower() == 'urgent': + node_id = get_node_id_from_num(sender_id, interface) + allowed_nodes = interface.allowed_nodes + if allowed_nodes and node_id not in allowed_nodes: + send_message("You don't have permission to post to this board.", sender_id, interface) + return send_message("What is the subject of your bulletin? Keep it short.", sender_id, interface) update_user_state(sender_id, {'command': 'BULLETIN_POST', 'step': 4, 'board': board_name}) @@ -182,7 +188,6 @@ def handle_bb_steps(sender_id, message, step, state, interface, bbs_nodes): update_user_state(sender_id, state) - def handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes): if step == 1: choice = message.lower() diff --git a/config_init.py b/config_init.py index 17820b7..f1ad993 100644 --- a/config_init.py +++ b/config_init.py @@ -80,9 +80,11 @@ def merge_config(system_config:dict[str, Any], args:argparse.Namespace) -> dict[ return system_config -def initialize_config(config_file:str = None) -> dict[str, Any]: - """Function reads and parses system configuration file - + +def initialize_config(config_file: str = None) -> dict[str, Any]: + """ + Function reads and parses system configuration file + Returns a dict with the following entries: config - parsed config file interface_type - type of the active interface @@ -97,24 +99,38 @@ def initialize_config(config_file:str = None) -> dict[str, Any]: dict: dict with system configuration, ad described above """ config = configparser.ConfigParser() - + if config_file is None: config_file = "config.ini" config.read(config_file) interface_type = config['interface']['type'] hostname = config['interface'].get('hostname', None) - port = config['interface'].get('port', None) + port = config['interface'].get('port', None) bbs_nodes = config.get('sync', 'bbs_nodes', fallback='').split(',') if bbs_nodes == ['']: bbs_nodes = [] - return {'config':config, 'interface_type': interface_type, 'hostname': hostname, 'port': port, 'bbs_nodes': bbs_nodes, 'mqtt_topic': 'meshtastic.receive'} + allowed_nodes = config.get('allow_list', 'allowed_nodes', fallback='').split(',') + if allowed_nodes == ['']: + allowed_nodes = [] + + return { + 'config': config, + 'interface_type': interface_type, + 'hostname': hostname, + 'port': port, + 'bbs_nodes': bbs_nodes, + 'allowed_nodes': allowed_nodes, + 'mqtt_topic': 'meshtastic.receive' + } + def get_interface(system_config:dict[str, Any]) -> meshtastic.stream_interface.StreamInterface: - """Function opens and returns an instance meshtastic interface of type specified by the configuration + """ + Function opens and returns an instance meshtastic interface of type specified by the configuration Function creates and returns an instance of a class inheriting from meshtastic.stream_interface.StreamInterface. The type of the class depends on the type of the interface specified by the system configuration. diff --git a/server.py b/server.py index a31511a..52ce1f7 100644 --- a/server.py +++ b/server.py @@ -39,19 +39,17 @@ Meshtastic Version def main(): display_banner() - # config, interface_type, hostname, port, bbs_nodes = initialize_config() args = init_cli_parser() config_file = None if args.config is not None: config_file = args.config system_config = initialize_config(config_file) - + merge_config(system_config, args) - - # print(f"{system_config=}") - + interface = get_interface(system_config) interface.bbs_nodes = system_config['bbs_nodes'] + interface.allowed_nodes = system_config['allowed_nodes'] logging.info(f"TCΒ²-BBS is running on {system_config['interface_type']} interface...") From 35c341e6e61881202ff1590029610e3b83dd28e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:07:57 -0400 Subject: [PATCH 19/30] Update example_config.ini --- example_config.ini | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/example_config.ini b/example_config.ini index b953fd1..4a1b029 100644 --- a/example_config.ini +++ b/example_config.ini @@ -32,3 +32,16 @@ type = serial # [sync] # bbs_nodes = !17d7e4b7 + + +############################ +#### Allowed Node IDs #### +############################ +# Provide a list of node IDs that are allowed to post to the urgent board. +# If this section is commented out, anyone can post to the urgent board. +# Example: +# [allow_list] +# allowed_nodes = 12345678,87654321 +# +# [allow_list] +# allowed_nodes = !17d7e4b7 From 15d836db2765e00e016d6859356b65d6d85efe7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:46:34 -0400 Subject: [PATCH 20/30] Added Reply option and some fixed Added reply option and some fixes for the Urgent board permissions --- command_handlers.py | 25 ++++++++++++++++++------- config_init.py | 4 ++++ db_operations.py | 8 ++++++++ server.py | 2 -- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/command_handlers.py b/command_handlers.py index 49df599..ee5e92d 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -8,7 +8,7 @@ from db_operations import ( add_bulletin, add_mail, delete_mail, get_bulletin_content, get_bulletins, get_mail, get_mail_content, - add_channel, get_channels + add_channel, get_channels, get_sender_id_by_mail_id ) from utils import ( get_node_id_from_num, get_node_info, @@ -150,8 +150,10 @@ def handle_bb_steps(sender_id, message, step, state, interface, bbs_nodes): if board_name.lower() == 'urgent': node_id = get_node_id_from_num(sender_id, interface) allowed_nodes = interface.allowed_nodes + print(f"Checking permissions for node_id: {node_id} with allowed_nodes: {allowed_nodes}") # Debug statement if allowed_nodes and node_id not in allowed_nodes: send_message("You don't have permission to post to this board.", sender_id, interface) + handle_bb_steps(sender_id, 'e', 1, state, interface, bbs_nodes) return send_message("What is the subject of your bulletin? Keep it short.", sender_id, interface) update_user_state(sender_id, {'command': 'BULLETIN_POST', 'step': 4, 'board': board_name}) @@ -188,6 +190,7 @@ def handle_bb_steps(sender_id, message, step, state, interface, bbs_nodes): update_user_state(sender_id, state) + def handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes): if step == 1: choice = message.lower() @@ -214,8 +217,8 @@ def handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes): sender_node_id = get_node_id_from_num(sender_id, interface) sender, date, subject, content, unique_id = get_mail_content(mail_id, sender_node_id) send_message(f"Date: {date}\nFrom: {sender}\nSubject: {subject}\n{content}", sender_id, interface) - send_message("Would you like to delete this message now that you've viewed it? Y/N", sender_id, interface) - update_user_state(sender_id, {'command': 'MAIL', 'step': 4, 'mail_id': mail_id, 'unique_id': unique_id}) + send_message("What would you like to do with this message?\n[K]eep [D]elete [R]eply", sender_id, interface) + update_user_state(sender_id, {'command': 'MAIL', 'step': 4, 'mail_id': mail_id, 'unique_id': unique_id, 'sender': sender, 'subject': subject, 'content': content}) except TypeError: logging.info(f"Node {sender_id} tried to access non-existent message") send_message("Mail not found", sender_id, interface) @@ -239,14 +242,19 @@ def handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes): update_user_state(sender_id, {'command': 'MAIL', 'step': 6, 'nodes': nodes}) elif step == 4: - if message.lower() == "y": + if message.lower() == "d": unique_id = state['unique_id'] sender_node_id = get_node_id_from_num(sender_id, interface) delete_mail(unique_id, sender_node_id, bbs_nodes, interface) send_message("The message has been deleted πŸ—‘οΈ", sender_id, interface) + update_user_state(sender_id, None) + elif message.lower() == "r": + sender = state['sender'] + send_message(f"Send your reply to {sender} now, followed by a message with END", sender_id, interface) + update_user_state(sender_id, {'command': 'MAIL', 'step': 7, 'reply_to_mail_id': state['mail_id'], 'subject': f"Re: {state['subject']}", 'content': ''}) else: send_message("The message has been kept in your inbox.βœ‰οΈ", sender_id, interface) - update_user_state(sender_id, None) + update_user_state(sender_id, None) elif step == 5: subject = message @@ -263,10 +271,14 @@ def handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes): elif step == 7: if message.lower() == "end": - recipient_id = state['recipient_id'] + if 'reply_to_mail_id' in state: + recipient_id = get_sender_id_by_mail_id(state['reply_to_mail_id']) # Get the sender ID from the mail ID + else: + recipient_id = state.get('recipient_id') subject = state['subject'] content = state['content'] recipient_name = get_node_name(recipient_id, interface) + sender_short_name = get_node_short_name(get_node_id_from_num(sender_id, interface), interface) unique_id = add_mail(get_node_id_from_num(sender_id, interface), sender_short_name, recipient_id, subject, content, bbs_nodes, interface) send_message(f"Mail has been posted to the mailbox of {recipient_name}.\n(β•―Β°β–‘Β°)β•―πŸ“¨πŸ“¬", sender_id, interface) @@ -288,7 +300,6 @@ def handle_mail_steps(sender_id, message, step, state, interface, bbs_nodes): update_user_state(sender_id, None) - def handle_wall_of_shame_command(sender_id, interface): response = "Devices with battery levels below 20%:\n" for node_id, node in interface.nodes.items(): diff --git a/config_init.py b/config_init.py index f1ad993..cb0e317 100644 --- a/config_init.py +++ b/config_init.py @@ -112,10 +112,14 @@ def initialize_config(config_file: str = None) -> dict[str, Any]: if bbs_nodes == ['']: bbs_nodes = [] + print(f"Configured to sync with the following BBS nodes: {bbs_nodes}") + allowed_nodes = config.get('allow_list', 'allowed_nodes', fallback='').split(',') if allowed_nodes == ['']: allowed_nodes = [] + print(f"Nodes with Urgent board permissions: {allowed_nodes}") + return { 'config': config, 'interface_type': interface_type, diff --git a/db_operations.py b/db_operations.py index de60722..4ce5061 100644 --- a/db_operations.py +++ b/db_operations.py @@ -156,3 +156,11 @@ def delete_mail(unique_id, recipient_id, bbs_nodes, interface): raise +def get_sender_id_by_mail_id(mail_id): + conn = get_db_connection() + c = conn.cursor() + c.execute("SELECT sender FROM mail WHERE id = ?", (mail_id,)) + result = c.fetchone() + if result: + return result[0] + return None diff --git a/server.py b/server.py index 52ce1f7..b0f1123 100644 --- a/server.py +++ b/server.py @@ -35,8 +35,6 @@ Meshtastic Version """ print(banner) - - def main(): display_banner() args = init_cli_parser() From 5a2db4b5f5b0cdbd6114d148a3963a4c69032e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:39:49 -0400 Subject: [PATCH 21/30] Added database admin script This adds a simple database admin script --- db_admin.py | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 db_admin.py diff --git a/db_admin.py b/db_admin.py new file mode 100644 index 0000000..78b6522 --- /dev/null +++ b/db_admin.py @@ -0,0 +1,201 @@ +import logging +import sqlite3 +import threading +import uuid +from datetime import datetime +import os + +# Import functions from meshtastic and utils as needed + +thread_local = threading.local() + +def get_db_connection(): + if not hasattr(thread_local, 'connection'): + thread_local.connection = sqlite3.connect('bulletins.db') + return thread_local.connection + +def initialize_database(): + conn = get_db_connection() + c = conn.cursor() + c.execute('''CREATE TABLE IF NOT EXISTS bulletins ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + board TEXT NOT NULL, + sender_short_name TEXT NOT NULL, + date TEXT NOT NULL, + subject TEXT NOT NULL, + content TEXT NOT NULL, + unique_id TEXT NOT NULL + )''') + c.execute('''CREATE TABLE IF NOT EXISTS mail ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sender TEXT NOT NULL, + sender_short_name TEXT NOT NULL, + recipient TEXT NOT NULL, + date TEXT NOT NULL, + subject TEXT NOT NULL, + content TEXT NOT NULL, + unique_id TEXT NOT NULL + );''') + c.execute('''CREATE TABLE IF NOT EXISTS channels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + url TEXT NOT NULL + );''') + conn.commit() + +def list_bulletins(): + conn = get_db_connection() + c = conn.cursor() + c.execute("SELECT id, board, sender_short_name, date, subject, unique_id FROM bulletins") + bulletins = c.fetchall() + if bulletins: + print_bold("Bulletins:") + for bulletin in bulletins: + print_bold(f"(ID: {bulletin[0]}, Board: {bulletin[1]}, Poster: {bulletin[2]}, Subject: {bulletin[4]})") + else: + print_bold("No bulletins found.") + print_separator() + return bulletins + +def list_mail(): + conn = get_db_connection() + c = conn.cursor() + c.execute("SELECT id, sender, sender_short_name, recipient, date, subject, unique_id FROM mail") + mail = c.fetchall() + if mail: + print_bold("Mail:") + for mail in mail: + print_bold(f"(ID: {mail[0]}, Sender: {mail[2]}, Recipient: {mail[3]}, Subject: {mail[5]})") + else: + print_bold("No mail found.") + print_separator() + return mail + +def list_channels(): + conn = get_db_connection() + c = conn.cursor() + c.execute("SELECT id, name, url FROM channels") + channels = c.fetchall() + if channels: + print_bold("Channels:") + for channel in channels: + print_bold(f"(ID: {channel[0]}, Name: {channel[1]}, URL: {channel[2]})") + else: + print_bold("No channels found.") + print_separator() + return channels + +def delete_bulletin(): + bulletins = list_bulletins() + if bulletins: + bulletin_ids = input_bold("Enter the bulletin ID(s) to delete (comma-separated) or 'X' to cancel: ").split(',') + if 'X' in [id.strip().upper() for id in bulletin_ids]: + print_bold("Deletion cancelled.") + print_separator() + return + conn = get_db_connection() + c = conn.cursor() + for bulletin_id in bulletin_ids: + c.execute("DELETE FROM bulletins WHERE id = ?", (bulletin_id.strip(),)) + conn.commit() + print_bold(f"Bulletin(s) with ID(s) {', '.join(bulletin_ids)} deleted.") + print_separator() + +def delete_mail(): + mail = list_mail() + if mail: + mail_ids = input_bold("Enter the mail ID(s) to delete (comma-separated) or 'X' to cancel: ").split(',') + if 'X' in [id.strip().upper() for id in mail_ids]: + print_bold("Deletion cancelled.") + print_separator() + return + conn = get_db_connection() + c = conn.cursor() + for mail_id in mail_ids: + c.execute("DELETE FROM mail WHERE id = ?", (mail_id.strip(),)) + conn.commit() + print_bold(f"Mail with ID(s) {', '.join(mail_ids)} deleted.") + print_separator() + +def delete_channel(): + channels = list_channels() + if channels: + channel_ids = input_bold("Enter the channel ID(s) to delete (comma-separated) or 'X' to cancel: ").split(',') + if 'X' in [id.strip().upper() for id in channel_ids]: + print_bold("Deletion cancelled.") + print_separator() + return + conn = get_db_connection() + c = conn.cursor() + for channel_id in channel_ids: + c.execute("DELETE FROM channels WHERE id = ?", (channel_id.strip(),)) + conn.commit() + print_bold(f"Channel(s) with ID(s) {', '.join(channel_ids)} deleted.") + print_separator() + +def display_menu(): + print("Menu:") + print("1. List Bulletins") + print("2. List Mail") + print("3. List Channels") + print("4. Delete Bulletin") + print("5. Delete Mail") + print("6. Delete Channel") + print("7. Exit") + +def display_banner(): + banner = """ +β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— +β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β•β•β•β•šβ•β•β•β•β–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β• + β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— + β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β•β• β•šβ•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β•šβ•β•β•β•β–ˆβ–ˆβ•‘ + β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘ + β•šβ•β• β•šβ•β•β•β•β•β•β•šβ•β•β•β•β•β•β• β•šβ•β•β•β•β•β• β•šβ•β•β•β•β•β• β•šβ•β•β•β•β•β•β• +Database Administrator +""" + print_bold(banner) + print_separator() + +def clear_screen(): + # Clear the console screen + os.system('cls' if os.name == 'nt' else 'clear') + +def input_bold(prompt): + print("\033[1m") # ANSI escape code for bold text + response = input(prompt) + print("\033[0m") # ANSI escape code to reset text + return response + +def print_bold(message): + print("\033[1m" + message + "\033[0m") # Bold text + +def print_separator(): + print_bold("========================") + +def main(): + display_banner() + initialize_database() + while True: + display_menu() + choice = input_bold("Enter your choice: ") + clear_screen() + if choice == '1': + list_bulletins() + elif choice == '2': + list_mail() + elif choice == '3': + list_channels() + elif choice == '4': + delete_bulletin() + elif choice == '5': + delete_mail() + elif choice == '6': + delete_channel() + elif choice == '7': + break + else: + print_bold("Invalid choice. Please try again.") + print_separator() + +if __name__ == "__main__": + main() From 89e8b041eba7a7480396608ad4384fc6d1647f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:47:43 -0400 Subject: [PATCH 22/30] Update db_admin.py --- db_admin.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/db_admin.py b/db_admin.py index 78b6522..8ed54ee 100644 --- a/db_admin.py +++ b/db_admin.py @@ -1,11 +1,6 @@ -import logging +import os import sqlite3 import threading -import uuid -from datetime import datetime -import os - -# Import functions from meshtastic and utils as needed thread_local = threading.local() @@ -157,7 +152,6 @@ Database Administrator print_separator() def clear_screen(): - # Clear the console screen os.system('cls' if os.name == 'nt' else 'clear') def input_bold(prompt): From 8946a81760778c1c91e6db40de074e1fa29b6edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Wed, 10 Jul 2024 23:26:41 -0400 Subject: [PATCH 23/30] Rename example_fortunes.txt to fortunes.txt --- example_fortunes.txt => fortunes.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename example_fortunes.txt => fortunes.txt (99%) diff --git a/example_fortunes.txt b/fortunes.txt similarity index 99% rename from example_fortunes.txt rename to fortunes.txt index 971870b..5dadf29 100644 --- a/example_fortunes.txt +++ b/fortunes.txt @@ -97,4 +97,4 @@ Mashed potatoes can be your friend. Never wrestle a pig. You both get dirty and the pig likes it. Nice one. You got away with it. Roll again. -SHHHHHH. listen. \ No newline at end of file +SHHHHHH. listen. From c7fbc4eb5f3465d6ae4a36c1019428c8ea089459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Thu, 11 Jul 2024 08:07:20 -0400 Subject: [PATCH 24/30] Fix CM,, issue Fix issue where CM,, does not display the command format help message --- command_handlers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/command_handlers.py b/command_handlers.py index ee5e92d..3869429 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -480,12 +480,13 @@ def handle_post_bulletin_command(sender_id, message, interface, bbs_nodes): def handle_check_bulletin_command(sender_id, message, interface): try: - parts = message.split(",,", 2) - if len(parts) != 2: + # Split the message only once + parts = message.split(",,", 1) + if len(parts) != 2 or not parts[1].strip(): send_message("Check Bulletins Quick Command format:\nCB,,{board_name}", sender_id, interface) return - _, board_name = parts + board_name = parts[1].strip() bulletins = get_bulletins(board_name) if not bulletins: send_message(f"No bulletins available on {board_name} board.", sender_id, interface) From 1891ea2776f323d1e687904f12c3d489b15d7965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Thu, 11 Jul 2024 09:52:35 -0400 Subject: [PATCH 25/30] Update command_handlers.py Change wording to add URL or PSK for channel dir --- command_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command_handlers.py b/command_handlers.py index 3869429..9f19109 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -349,7 +349,7 @@ def handle_channel_directory_steps(sender_id, message, step, state, interface): elif step == 3: channel_name = message - send_message("Send a message with your channel URL:", sender_id, interface) + send_message("Send a message with your channel URL or PSK:", sender_id, interface) update_user_state(sender_id, {'command': 'CHANNEL_DIRECTORY', 'step': 4, 'channel_name': channel_name}) elif step == 4: From 048772af0febfcebc5fbdd259e949b31b99aef1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Thu, 11 Jul 2024 10:52:17 -0400 Subject: [PATCH 26/30] Update db_admin.py Wording change --- db_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db_admin.py b/db_admin.py index 8ed54ee..6a585dd 100644 --- a/db_admin.py +++ b/db_admin.py @@ -133,9 +133,9 @@ def display_menu(): print("1. List Bulletins") print("2. List Mail") print("3. List Channels") - print("4. Delete Bulletin") + print("4. Delete Bulletins") print("5. Delete Mail") - print("6. Delete Channel") + print("6. Delete Channels") print("7. Exit") def display_banner(): From f7e40985f7b5b25937241881bfb2e12dd66bd49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:35:10 -0400 Subject: [PATCH 27/30] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 68730ba..c743287 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ If you're a Docker user, TCΒ²-BBS Meshtastic is available on Docker Hub! ``` 6. Set up the configuration in `config.ini`: + + You'll need to open up the config.ini file in a text editor and make your changes following the instructions below **[interface]** If using `type = serial` and you have multiple devices connected, you will need to uncomment the `port =` line and enter the port of your device. 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 28/30] 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() From 63b2aedf51b188693a34fb8c0a2237f780b5d8ef 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:54:03 -0400 Subject: [PATCH 29/30] Update example_config.ini --- example_config.ini | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/example_config.ini b/example_config.ini index 4a1b029..1b6913a 100644 --- a/example_config.ini +++ b/example_config.ini @@ -45,3 +45,23 @@ type = serial # # [allow_list] # allowed_nodes = !17d7e4b7 + + +########################## +#### JS8Call Settings #### +########################## +# If you would like messages from JS8Call to go into the BBS, uncomment and enter in info below: +# host = the IP address for your system running JS8Call +# port = TCP API port for JS8CALL - Default is 2442 +# db_file = this can be left as the default "js8call.db" unless you need to change for some reason +# js8groups = the JS8Call groups you're interested in receiving into the BBS +# store_messages = "true" will send messages that arent part of a group into the BBS (can be noisy). "false" will ignore these +# js8urgent = the JS8Call groups you consider to be urgent - anything sent to these will have a notice sent to the +# group chat (similar to how the urgent bulletin board works +# [js8call] +# host = 192.168.1.100 +# port = 2442 +# db_file = js8call.db +# js8groups = @GRP1,@GRP2,@GRP3 +# store_messages = True +# js8urgent = @URGNT From eaf2fbb8f8a5f51c5e0b5b9877d1f13ee47192f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TC=C2=B2?= <130875305+TheCommsChannel@users.noreply.github.com> Date: Mon, 15 Jul 2024 04:51:07 -0400 Subject: [PATCH 30/30] Clean up log output --- js8call_integration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js8call_integration.py b/js8call_integration.py index 46783ff..bf0a0cc 100644 --- a/js8call_integration.py +++ b/js8call_integration.py @@ -28,6 +28,8 @@ class JS8CallClient: def __init__(self, interface, logger=None): self.logger = logger or logging.getLogger('js8call') self.logger.setLevel(logging.INFO) + self.logger.propagate = False + self.config = configparser.ConfigParser() self.config.read(config_file) @@ -98,7 +100,6 @@ class JS8CallClient: 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}") @@ -127,7 +128,6 @@ class JS8CallClient: 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}") @@ -293,4 +293,4 @@ def handle_group_message_selection(sender_id, message, step, state, interface): send_message("Invalid group selection. Please choose again.", sender_id, interface) handle_group_messages_command(sender_id, interface) - handle_js8call_command(sender_id, interface) + handle_js8call_command(sender_id, interface) \ No newline at end of file