mirror of
https://github.com/TheCommsChannel/TC2-BBS-mesh.git
synced 2024-10-01 01:15:36 -04:00
commit
68640f8c0e
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,8 @@
|
||||
__pycache__/
|
||||
bulletins.db
|
||||
js8call.db
|
||||
venv/
|
||||
.venv
|
||||
.idea
|
||||
config.ini
|
||||
fortunes.txt
|
||||
|
20
README.md
20
README.md
@ -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
|
||||
|
||||
|
@ -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"
|
||||
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', 'step': 2, 'board': board_name})
|
||||
else:
|
||||
handle_help_command(sender_id, interface)
|
||||
update_user_state(sender_id, None)
|
||||
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']
|
||||
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,13 +240,18 @@ 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)
|
||||
send_message("The message has been kept in your inbox.✉️", sender_id, interface)
|
||||
update_user_state(sender_id, None)
|
||||
|
||||
elif step == 5:
|
||||
@ -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:
|
||||
@ -398,3 +356,255 @@ 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 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)
|
||||
|
34
config.ini
34
config.ini
@ -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
|
@ -80,8 +80,10 @@ 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
|
||||
@ -110,11 +112,29 @@ def initialize_config(config_file:str = None) -> dict[str, Any]:
|
||||
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
195
db_admin.py
Normal 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()
|
@ -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
67
example_config.ini
Normal 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
|
126
examples/example_RulesOfAcquisition_fortunes.txt
Normal file
126
examples/example_RulesOfAcquisition_fortunes.txt
Normal 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"
|
296
js8call_integration.py
Normal file
296
js8call_integration.py
Normal 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)
|
@ -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,10 +77,57 @@ 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)
|
||||
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:
|
||||
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']
|
||||
@ -59,9 +137,33 @@ def process_message(sender_id, message, interface, is_sync_message=False):
|
||||
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)
|
||||
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)
|
||||
|
||||
@ -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
|
||||
|
41
server.py
41
server.py
@ -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,11 +49,8 @@ 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:
|
||||
@ -48,20 +59,26 @@ def main():
|
||||
|
||||
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()
|
||||
|
8
utils.py
8
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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user