Merge pull request #1 from TheCommsChannel/main

Syncing with TC2
This commit is contained in:
pitbullcoder 2024-07-15 10:00:36 -04:00 committed by GitHub
commit 68640f8c0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1290 additions and 242 deletions

4
.gitignore vendored
View file

@ -1,4 +1,8 @@
__pycache__/
bulletins.db
js8call.db
venv/
.venv
.idea
config.ini
fortunes.txt

View file

@ -54,7 +54,15 @@ 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`:
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.
@ -159,7 +167,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
@ -171,14 +179,18 @@ 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
```
## 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 have been working:
- **Client**
- **Router_Client**
## Features

View file

@ -2,12 +2,13 @@ import logging
import random
import time
from config_init import initialize_config
from meshtastic import BROADCAST_NUM
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,
@ -16,6 +17,19 @@ 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\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:
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:
@ -24,15 +38,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):
@ -40,47 +55,8 @@ def handle_exit_command(sender_id, interface):
update_user_state(sender_id, None)
def handle_help_command(sender_id, interface, state=None):
title = "█▓▒░ TC² 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"
]
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 +75,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 +110,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 +119,56 @@ 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: "News", 2: "Info", 3: "Urgent"}
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 = 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})
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':
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', '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,52 +182,48 @@ 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})
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:
# 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:
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)
@ -290,14 +240,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.✉️\nJust don't let it get as messy as your regular email inbox (ಠ_ಠ)", sender_id, interface)
update_user_state(sender_id, None)
send_message("The message has been kept in your inbox.✉️", sender_id, interface)
update_user_state(sender_id, None)
elif step == 5:
subject = message
@ -314,16 +269,19 @@ 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)
# 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)
@ -354,18 +312,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(
@ -375,7 +333,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})
@ -389,7 +347,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:
@ -397,4 +355,256 @@ 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)
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 CM."
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]}\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)
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:
# 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[1].strip()
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_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)

View file

@ -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/ttyUSB0
# 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

View file

@ -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,42 @@ 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'}
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,
'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.

195
db_admin.py Normal file
View file

@ -0,0 +1,195 @@
import os
import sqlite3
import threading
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 Bulletins")
print("5. Delete Mail")
print("6. Delete Channels")
print("7. Exit")
def display_banner():
banner = """
Database Administrator
"""
print_bold(banner)
print_separator()
def clear_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()

View file

@ -4,11 +4,13 @@ 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,
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
)
@ -49,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()
@ -78,12 +84,12 @@ 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
def get_bulletins(board):
conn = get_db_connection()
c = conn.cursor()
@ -131,17 +137,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)
@ -149,3 +154,13 @@ 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
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

67
example_config.ini Normal file
View file

@ -0,0 +1,67 @@
###############################
#### 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
############################
#### 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
##########################
#### 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

View file

@ -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"

View file

@ -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.
SHHHHHH. listen.

296
js8call_integration.py Normal file
View file

@ -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.logger.propagate = False
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))
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))
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)

View file

@ -1,24 +1,56 @@
import logging
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
)
from meshtastic import BROADCAST_NUM
from db_operations import add_bulletin, add_mail, delete_bulletin, delete_mail
from command_handlers import (
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 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
command_handlers = {
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,
"j": handle_js8call_command,
"x": handle_help_command
}
utilities_menu_handlers = {
"s": handle_stats_command,
"f": handle_fortune_command,
"w": handle_wall_of_shame_command,
"exit": handle_exit_command,
"h": handle_help_command,
"c": handle_channel_directory_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):
@ -33,9 +65,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]
@ -46,24 +77,95 @@ 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)
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)
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_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
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
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)
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:
@ -94,3 +196,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

View file

@ -2,8 +2,8 @@
"""
TC²-BBS Server for Meshtastic by TheCommsChannel (TC²)
Date: 06/25/2024
Version: 0.1.0
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 = """
@ -35,33 +49,36 @@ Meshtastic Version
"""
print(banner)
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...")
initialize_database()
def receive_packet(packet):
def receive_packet(packet, interface):
on_receive(packet, interface)
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)
@ -69,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()

View file

@ -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
@ -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)