mirror of
https://github.com/TheCommsChannel/TC2-BBS-mesh.git
synced 2024-11-09 22:24:06 -08:00
Add files via upload
This commit is contained in:
parent
36ac328636
commit
339210401f
109
README.md
Normal file
109
README.md
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
# TC²-BBS Meshtastic Version
|
||||||
|
|
||||||
|
This is the TC²-BBS system integrated with Meshtastic devices. The system allows for message handling, bulletin boards, mail systems, and a channel directory.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Python 3.x
|
||||||
|
- Meshtastic
|
||||||
|
- pypubsub
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/TheCommsChannel/TC2-BBS-mesh.git
|
||||||
|
cd TC2-BBS-mesh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Set up a Python virtual environment:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python -m venv venv
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Activate the virtual environment:
|
||||||
|
|
||||||
|
|
||||||
|
- On Windows:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
- On macOS and Linux:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Install the required packages:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Set up the configuration in `config.ini`:
|
||||||
|
|
||||||
|
**[interface]**
|
||||||
|
|
||||||
|
If using `type = serial` and you have multiple devices connected, you will need to uncomment the `port =` line and enter in the port of your device.
|
||||||
|
|
||||||
|
Linux Example:
|
||||||
|
`port = /dev/ttyUSB0`
|
||||||
|
|
||||||
|
Windows Example:
|
||||||
|
`port = COM3`
|
||||||
|
|
||||||
|
If using type = tcp you will need to uncomment the hostname = 192.168.x.x line and put in the IP address of your Meshtastic device
|
||||||
|
|
||||||
|
**[sync]**
|
||||||
|
|
||||||
|
Enter in a list of other BBS nodes you would like to sync messages and bulletins with. Separate each by comma and no spaces as shown in the example below.
|
||||||
|
You can find the nodeID in the menu under `Radio Configuration > User` for each node, or use this script for getting nodedb data from a device:
|
||||||
|
|
||||||
|
[Meshtastic-Python-Examples/print-nodedb.py at main · pdxlocations/Meshtastic-Python-Examples (github.com)](https://github.com/pdxlocations/Meshtastic-Python-Examples/blob/main/print-nodedb.py)
|
||||||
|
|
||||||
|
Example Config:
|
||||||
|
```ini
|
||||||
|
[interface]
|
||||||
|
type = serial
|
||||||
|
# port = /dev/ttyUSB0
|
||||||
|
# hostname = 192.168.x.x
|
||||||
|
|
||||||
|
[sync]
|
||||||
|
bbs_nodes = !f53f4abc,!f3abc123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Server
|
||||||
|
|
||||||
|
Run the server with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Be sure you've followed the Python virtual environment steps above and activated it before running.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Mail System**: Send and receive mail messages.
|
||||||
|
- **Bulletin Boards**: Post and view bulletins on various boards.
|
||||||
|
- **Channel Directory**: Add and view channels in the directory.
|
||||||
|
- **Statistics**: View statistics about nodes, hardware, and roles.
|
||||||
|
- **Wall of Shame**: View devices with low battery levels.
|
||||||
|
- **Fortune Teller**: Get a random fortune. Pulls from the fortunes.txt file. Feel free to edit this file remove or add more if you like.
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
Big thanks to [Meshtastic]([Meshtastic (github.com)](https://github.com/meshtastic)) and [pdxlocations](https://github.com/pdxlocations) for the great Python examples:
|
||||||
|
[python/examples at master · meshtastic/python (github.com)](https://github.com/meshtastic/python/tree/master/examples)
|
||||||
|
|
||||||
|
[pdxlocations/Meshtastic-Python-Examples (github.com)](https://github.com/pdxlocations/Meshtastic-Python-Examples)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GNU General Public License v3.0
|
392
command_handlers.py
Normal file
392
command_handlers.py
Normal file
|
@ -0,0 +1,392 @@
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
from config_init import initialize_config
|
||||||
|
from db_operations import (
|
||||||
|
add_bulletin, add_mail, delete_mail,
|
||||||
|
get_bulletin_content, get_bulletins,
|
||||||
|
get_mail, get_mail_content,
|
||||||
|
add_channel, get_channels
|
||||||
|
)
|
||||||
|
from utils import (
|
||||||
|
get_node_id_from_num, get_node_info,
|
||||||
|
get_node_short_name, send_message,
|
||||||
|
update_user_state
|
||||||
|
)
|
||||||
|
|
||||||
|
config, interface_type, hostname, port, bbs_nodes = initialize_config()
|
||||||
|
|
||||||
|
|
||||||
|
def get_node_name(node_id, interface):
|
||||||
|
node_info = interface.nodes.get(node_id)
|
||||||
|
if node_info:
|
||||||
|
return node_info['user']['longName']
|
||||||
|
return f"Node {node_id}"
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
send_message(response, sender_id, interface)
|
||||||
|
update_user_state(sender_id, {'command': 'BULLETIN', 'step': 1})
|
||||||
|
|
||||||
|
|
||||||
|
def handle_exit_command(sender_id, interface):
|
||||||
|
send_message("Type 'HELP' for a list of commands.", 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"
|
||||||
|
send_message(response, sender_id, interface)
|
||||||
|
update_user_state(sender_id, {'command': 'STATS', 'step': 1})
|
||||||
|
|
||||||
|
|
||||||
|
def handle_fortune_command(sender_id, interface):
|
||||||
|
try:
|
||||||
|
with open('fortunes.txt', 'r') as file:
|
||||||
|
fortunes = file.readlines()
|
||||||
|
if not fortunes:
|
||||||
|
send_message("No fortunes available.", sender_id, interface)
|
||||||
|
return
|
||||||
|
fortune = random.choice(fortunes).strip()
|
||||||
|
decorated_fortune = f"🔮 {fortune} 🔮"
|
||||||
|
send_message(decorated_fortune, sender_id, interface)
|
||||||
|
except Exception as e:
|
||||||
|
send_message(f"Error generating fortune: {e}", sender_id, interface)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_stats_steps(sender_id, message, step, interface, bbs_nodes):
|
||||||
|
if step == 1:
|
||||||
|
choice = message.upper()
|
||||||
|
if choice == '3':
|
||||||
|
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"
|
||||||
|
send_message(response, sender_id, interface)
|
||||||
|
update_user_state(sender_id, {'command': 'STATS', 'step': 2})
|
||||||
|
elif choice == 1:
|
||||||
|
hw_models = {}
|
||||||
|
for node in interface.nodes.values():
|
||||||
|
hw_model = node['user'].get('hwModel', 'Unknown')
|
||||||
|
hw_models[hw_model] = hw_models.get(hw_model, 0) + 1
|
||||||
|
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:
|
||||||
|
roles = {}
|
||||||
|
for node in interface.nodes.values():
|
||||||
|
role = node['user'].get('role', 'Unknown')
|
||||||
|
roles[role] = roles.get(role, 0) + 1
|
||||||
|
response = "Roles:\n" + "\n".join([f"{role}: {count}" for role, count in roles.items()])
|
||||||
|
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"}
|
||||||
|
|
||||||
|
if step == 1:
|
||||||
|
if message == '4':
|
||||||
|
handle_help_command(sender_id, interface)
|
||||||
|
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)
|
||||||
|
|
||||||
|
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']
|
||||||
|
bulletins = get_bulletins(board_name)
|
||||||
|
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})
|
||||||
|
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':
|
||||||
|
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']})
|
||||||
|
|
||||||
|
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})
|
||||||
|
|
||||||
|
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': ''})
|
||||||
|
|
||||||
|
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']
|
||||||
|
content = state['content']
|
||||||
|
node_id = get_node_id_from_num(sender_id, interface)
|
||||||
|
node_info = interface.nodes.get(node_id)
|
||||||
|
if node_info is None:
|
||||||
|
send_message("Error: Unable to retrieve your node information.", sender_id, interface)
|
||||||
|
update_user_state(sender_id, None)
|
||||||
|
return
|
||||||
|
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})
|
||||||
|
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':
|
||||||
|
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)
|
||||||
|
update_user_state(sender_id, {'command': 'MAIL', 'step': 2})
|
||||||
|
else:
|
||||||
|
send_message("There are no messages in your mailbox.\n(`⌒`)", sender_id, interface)
|
||||||
|
update_user_state(sender_id, None)
|
||||||
|
elif choice == '1':
|
||||||
|
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':
|
||||||
|
handle_help_command(sender_id, interface)
|
||||||
|
|
||||||
|
elif step == 2:
|
||||||
|
mail_id = int(message)
|
||||||
|
sender, date, subject, content, unique_id = get_mail_content(mail_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})
|
||||||
|
|
||||||
|
elif step == 3:
|
||||||
|
short_name = message
|
||||||
|
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)
|
||||||
|
handle_mail_command(sender_id, interface)
|
||||||
|
elif len(nodes) == 1:
|
||||||
|
recipient_id = nodes[0]['num']
|
||||||
|
recipient_name = get_node_name(recipient_id, interface)
|
||||||
|
send_message(f"What is the subject of your message to {recipient_name}?\nKeep it short.", sender_id, interface)
|
||||||
|
update_user_state(sender_id, {'command': 'MAIL', 'step': 5, 'recipient_id': recipient_id})
|
||||||
|
else:
|
||||||
|
send_message("There are multiple nodes with that short name. Which one would you like to leave a message for?", sender_id, interface)
|
||||||
|
for i, node in enumerate(nodes):
|
||||||
|
send_message(f"[{i}] {node['longName']}", sender_id, interface)
|
||||||
|
update_user_state(sender_id, {'command': 'MAIL', 'step': 6, 'nodes': nodes})
|
||||||
|
|
||||||
|
elif step == 4:
|
||||||
|
if message.lower() == "y":
|
||||||
|
unique_id = state['unique_id']
|
||||||
|
delete_mail(unique_id, bbs_nodes, interface)
|
||||||
|
send_message("The message has been deleted 🗑️", sender_id, interface)
|
||||||
|
else:
|
||||||
|
send_message("The message has been kept in your inbox.✉️\nJust don't let it get as messy as your regular email inbox (ಠ_ಠ)", sender_id, interface)
|
||||||
|
update_user_state(sender_id, None)
|
||||||
|
|
||||||
|
elif step == 5:
|
||||||
|
subject = message
|
||||||
|
send_message("Send your message. You can send it in multiple messages if it's too long for one.\nSend a single message with END when you're done", sender_id, interface)
|
||||||
|
update_user_state(sender_id, {'command': 'MAIL', 'step': 7, 'recipient_id': state['recipient_id'], 'subject': subject, 'content': ''})
|
||||||
|
|
||||||
|
elif step == 6:
|
||||||
|
selected_node_index = int(message)
|
||||||
|
selected_node = state['nodes'][selected_node_index]
|
||||||
|
recipient_id = selected_node['num']
|
||||||
|
recipient_name = get_node_name(recipient_id, interface)
|
||||||
|
send_message(f"What is the subject of your message to {recipient_name}?\nKeep it short.", sender_id, interface)
|
||||||
|
update_user_state(sender_id, {'command': 'MAIL', 'step': 5, 'recipient_id': recipient_id})
|
||||||
|
|
||||||
|
elif step == 7:
|
||||||
|
if message.lower() == "end":
|
||||||
|
recipient_id = state['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."
|
||||||
|
send_message(notification_message, recipient_id, interface)
|
||||||
|
|
||||||
|
update_user_state(sender_id, None)
|
||||||
|
update_user_state(sender_id, {'command': 'MAIL', 'step': 8})
|
||||||
|
else:
|
||||||
|
state['content'] += message + "\n"
|
||||||
|
update_user_state(sender_id, state)
|
||||||
|
|
||||||
|
elif step == 8:
|
||||||
|
if message.lower() == "y":
|
||||||
|
handle_mail_command(sender_id, interface)
|
||||||
|
else:
|
||||||
|
send_message("Okay, feel free to send another command.", sender_id, interface)
|
||||||
|
update_user_state(sender_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_wall_of_shame_command(sender_id, interface):
|
||||||
|
response = "Devices with battery levels below 20%:\n"
|
||||||
|
for node_id, node in interface.nodes.items():
|
||||||
|
metrics = node.get('deviceMetrics', {})
|
||||||
|
battery_level = metrics.get('batteryLevel', 101)
|
||||||
|
if battery_level < 20:
|
||||||
|
long_name = node['user']['longName']
|
||||||
|
response += f"{long_name} - Battery {battery_level}%\n"
|
||||||
|
if response == "Devices with battery levels below 20%:\n":
|
||||||
|
response = "No devices with battery levels below 20% found."
|
||||||
|
send_message(response, 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"
|
||||||
|
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':
|
||||||
|
handle_help_command(sender_id, interface)
|
||||||
|
return
|
||||||
|
elif choice == '0':
|
||||||
|
channels = get_channels()
|
||||||
|
if channels:
|
||||||
|
response = "Select a channel number to view:\n" + "\n".join(
|
||||||
|
[f"[{i}] {channel[0]}" for i, channel in enumerate(channels)])
|
||||||
|
send_message(response, sender_id, interface)
|
||||||
|
update_user_state(sender_id, {'command': 'CHANNEL_DIRECTORY', 'step': 2})
|
||||||
|
else:
|
||||||
|
send_message("No channels available in the directory.", sender_id, interface)
|
||||||
|
handle_channel_directory_command(sender_id, interface)
|
||||||
|
elif choice == '1':
|
||||||
|
send_message("Name your channel for the directory:", sender_id, interface)
|
||||||
|
update_user_state(sender_id, {'command': 'CHANNEL_DIRECTORY', 'step': 3})
|
||||||
|
|
||||||
|
elif step == 2:
|
||||||
|
channel_index = int(message)
|
||||||
|
channels = get_channels()
|
||||||
|
if 0 <= channel_index < len(channels):
|
||||||
|
channel_name, channel_url = channels[channel_index]
|
||||||
|
send_message(f"Channel Name: {channel_name}\nChannel URL:\n{channel_url}", sender_id, interface)
|
||||||
|
handle_channel_directory_command(sender_id, interface)
|
||||||
|
|
||||||
|
elif step == 3:
|
||||||
|
channel_name = message
|
||||||
|
send_message("Send a message with your channel URL:", sender_id, interface)
|
||||||
|
update_user_state(sender_id, {'command': 'CHANNEL_DIRECTORY', 'step': 4, 'channel_name': channel_name})
|
||||||
|
|
||||||
|
elif step == 4:
|
||||||
|
channel_url = message
|
||||||
|
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)
|
34
config.ini
Normal file
34
config.ini
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
###############################
|
||||||
|
#### Select Interface type ####
|
||||||
|
###############################
|
||||||
|
# [type = serial] for USB connected devices
|
||||||
|
#If there are multiple serial devices connected, be sure to use the "port" option and specify a port
|
||||||
|
# Linux Example:
|
||||||
|
# port = /dev/ttyUSB0
|
||||||
|
#
|
||||||
|
# Windows Example:
|
||||||
|
# port = COM3
|
||||||
|
# [type = tcp] for network connected devices (ESP32 devices only - this does not work for WisBlock)
|
||||||
|
# If using tcp, remove the # from the beginning and replace 192.168.x.x with the IP address of your device
|
||||||
|
# Example:
|
||||||
|
# [interface]
|
||||||
|
# type = tcp
|
||||||
|
# hostname = 192.168.1.100
|
||||||
|
|
||||||
|
[interface]
|
||||||
|
type = serial
|
||||||
|
port = /dev/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
|
39
config_init.py
Normal file
39
config_init.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import configparser
|
||||||
|
import time
|
||||||
|
import meshtastic.serial_interface
|
||||||
|
import meshtastic.tcp_interface
|
||||||
|
import serial.tools.list_ports
|
||||||
|
|
||||||
|
def initialize_config():
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read('config.ini')
|
||||||
|
interface_type = config['interface']['type']
|
||||||
|
hostname = config['interface'].get('hostname', None)
|
||||||
|
port = config['interface'].get('port', None)
|
||||||
|
bbs_nodes = config['sync']['bbs_nodes'].split(',')
|
||||||
|
return config, interface_type, hostname, port, bbs_nodes
|
||||||
|
|
||||||
|
def get_interface(interface_type, hostname=None, port=None):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if interface_type == 'serial':
|
||||||
|
if port:
|
||||||
|
return meshtastic.serial_interface.SerialInterface(port)
|
||||||
|
else:
|
||||||
|
ports = list(serial.tools.list_ports.comports())
|
||||||
|
if len(ports) == 1:
|
||||||
|
return meshtastic.serial_interface.SerialInterface(ports[0].device)
|
||||||
|
elif len(ports) > 1:
|
||||||
|
port_list = ', '.join([p.device for p in ports])
|
||||||
|
raise ValueError(f"Multiple serial ports detected: {port_list}. Specify one with the 'port' argument.")
|
||||||
|
else:
|
||||||
|
raise ValueError("No serial ports detected.")
|
||||||
|
elif interface_type == 'tcp':
|
||||||
|
if not hostname:
|
||||||
|
raise ValueError("Hostname must be specified for TCP interface")
|
||||||
|
return meshtastic.tcp_interface.TCPInterface(hostname=hostname)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid interface type specified in config file")
|
||||||
|
except PermissionError as e:
|
||||||
|
print(f"PermissionError: {e}. Retrying in 5 seconds...")
|
||||||
|
time.sleep(5)
|
149
db_operations.py
Normal file
149
db_operations.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
import logging
|
||||||
|
import sqlite3
|
||||||
|
import threading
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
print("Database schema initialized.")
|
||||||
|
|
||||||
|
def add_channel(name, url):
|
||||||
|
conn = get_db_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("INSERT INTO channels (name, url) VALUES (?, ?)", (name, url))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def get_channels():
|
||||||
|
conn = get_db_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT name, url FROM channels")
|
||||||
|
return c.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def add_bulletin(board, sender_short_name, subject, content, bbs_nodes, interface, unique_id=None):
|
||||||
|
conn = get_db_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
date = datetime.now().strftime('%m/%d/%Y %H:%M')
|
||||||
|
if not unique_id:
|
||||||
|
unique_id = str(uuid.uuid4())
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO bulletins (board, sender_short_name, date, subject, content, unique_id) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
(board, sender_short_name, date, subject, content, unique_id))
|
||||||
|
conn.commit()
|
||||||
|
if bbs_nodes and interface:
|
||||||
|
send_bulletin_to_bbs_nodes(board, sender_short_name, subject, content, unique_id, bbs_nodes, interface)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
return unique_id
|
||||||
|
|
||||||
|
def get_bulletins(board):
|
||||||
|
conn = get_db_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT id, subject, sender_short_name, date, unique_id FROM bulletins WHERE board = ?", (board,))
|
||||||
|
return c.fetchall()
|
||||||
|
|
||||||
|
def get_bulletin_content(bulletin_id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT sender_short_name, date, subject, content, unique_id FROM bulletins WHERE id = ?", (bulletin_id,))
|
||||||
|
return c.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_bulletin(bulletin_id, bbs_nodes, interface):
|
||||||
|
conn = get_db_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("DELETE FROM bulletins WHERE id = ?", (bulletin_id,))
|
||||||
|
conn.commit()
|
||||||
|
send_delete_bulletin_to_bbs_nodes(bulletin_id, bbs_nodes, interface)
|
||||||
|
|
||||||
|
def add_mail(sender_id, sender_short_name, recipient_id, subject, content, bbs_nodes, interface, unique_id=None):
|
||||||
|
conn = get_db_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
date = datetime.now().strftime('%m/%d/%Y %H:%M')
|
||||||
|
if not unique_id:
|
||||||
|
unique_id = str(uuid.uuid4())
|
||||||
|
c.execute("INSERT INTO mail (sender, sender_short_name, recipient, date, subject, content, unique_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
(sender_id, sender_short_name, recipient_id, date, subject, content, unique_id))
|
||||||
|
conn.commit()
|
||||||
|
if bbs_nodes and interface:
|
||||||
|
send_mail_to_bbs_nodes(sender_id, sender_short_name, recipient_id, subject, content, unique_id, bbs_nodes, interface)
|
||||||
|
return unique_id
|
||||||
|
|
||||||
|
def get_mail(recipient_id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT id, sender_short_name, subject, date, unique_id FROM mail WHERE recipient = ?", (recipient_id,))
|
||||||
|
return c.fetchall()
|
||||||
|
|
||||||
|
def get_mail_content(mail_id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT sender_short_name, date, subject, content, unique_id FROM mail WHERE id = ?", (mail_id,))
|
||||||
|
return c.fetchone()
|
||||||
|
|
||||||
|
def delete_mail(unique_id, bbs_nodes, interface):
|
||||||
|
logging.info(f"Attempting to delete mail with unique_id: {unique_id}")
|
||||||
|
conn = get_db_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
try:
|
||||||
|
c.execute("SELECT unique_id 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
|
||||||
|
c.execute("DELETE FROM mail WHERE unique_id = ?", (unique_id,))
|
||||||
|
conn.commit()
|
||||||
|
send_delete_mail_to_bbs_nodes(unique_id, bbs_nodes, interface)
|
||||||
|
logging.info(f"Mail with unique_id: {unique_id} deleted and sync message sent.")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error deleting mail with unique_id {unique_id}: {e}")
|
||||||
|
raise
|
100
fortunes.txt
Normal file
100
fortunes.txt
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
Nothing is impossible. Except Monday mornings.
|
||||||
|
You are not illiterate.
|
||||||
|
A comfort zone is a magical place where nothing ever grows.
|
||||||
|
Give yourself a break before you breakdown.
|
||||||
|
If a true sense of value is to be yours it must come through service.
|
||||||
|
Happiness is enjoying what you got. Never from what you want.
|
||||||
|
Go confidently in the direction of your dreams.
|
||||||
|
Genius is eternal patience.
|
||||||
|
Follow the advice of your heart.
|
||||||
|
Follow the middle path. Neither extreme will make you happy.
|
||||||
|
Fight for it. You will come out on the top.
|
||||||
|
Failure is the tuition you pay for success.
|
||||||
|
Expect a change for the better.
|
||||||
|
Examine the situation before you act impulsively.
|
||||||
|
Everything you are against weakens you. Everything you are for empowers you.
|
||||||
|
Curiosity kills boredom. Nothing can kill curiosity.
|
||||||
|
Common sense is instinct. Enough of it is genius.
|
||||||
|
Call an old friend today.
|
||||||
|
Better ask twice than lose yourself once.
|
||||||
|
Behavior is a mirror in which everyone shows his own image.
|
||||||
|
Balance life with a little sweet & sour.
|
||||||
|
Attitude is a little thing that makes a BIG difference.
|
||||||
|
An unexpected check or fortune will arrive today.
|
||||||
|
An unexpected event will bring you riches.
|
||||||
|
An upset is an opportunity to see the truth.
|
||||||
|
Any rough times are behind you.
|
||||||
|
An important person will offer you support.
|
||||||
|
An investment in knowledge always pays the best interest.
|
||||||
|
An investment in yourself will pay dividends for the rest of your life.
|
||||||
|
An old wish will come true.
|
||||||
|
All your hard work will soon pay off.
|
||||||
|
All your sorrows will vanish.
|
||||||
|
All the little things will add to a happy journey.
|
||||||
|
All that we are is a result of what we have thought.
|
||||||
|
All the effort you are making will ultimately pay off.
|
||||||
|
Act boldly and unseen forces will come to your aid.
|
||||||
|
6 out of every 10 people refuse to be a statistic.
|
||||||
|
a belief is just a thought you keep having
|
||||||
|
A bit of fragrance clings to the hand that gives flowers.
|
||||||
|
Absolutes are absolutely wrong
|
||||||
|
abstraction is a type of decadence
|
||||||
|
Abuse of authority comes as no surprise.
|
||||||
|
Abuse of power comes as no surprise
|
||||||
|
Acting without thinking can be awfully entertaining.
|
||||||
|
Actions speak louder than words... but nothing speaks louder than they who take no action
|
||||||
|
Adapt or die.
|
||||||
|
Ad astra per aspera.
|
||||||
|
A fine will be charged for wounding the library books.
|
||||||
|
A good man has few enemies. A ruthless man has none.
|
||||||
|
A lack of leadership is no substitute for inaction.
|
||||||
|
Alarm clocks kill dreams.
|
||||||
|
A little knowledge can go a long way
|
||||||
|
A little madness now and then is relished by the wisest men.
|
||||||
|
A little rudeness and disrespect can elevate a meaningless interaction to a battle of wills and add drama to an otherwise dull day.
|
||||||
|
All employees are forbidden.
|
||||||
|
All this foliage is obstructing my scenic view.
|
||||||
|
Almost half of them are BELOW AVERAGE
|
||||||
|
Always be ready to walk away from a bad idea.
|
||||||
|
Always store beer in a dark place.
|
||||||
|
Annihilate the adversaries with laughter.
|
||||||
|
An object at rest cannot be stopped
|
||||||
|
Are You Mad At Yourself for Not Exiting
|
||||||
|
a sense of timing is the mark of genius
|
||||||
|
Asking questions is the only way to get answers.
|
||||||
|
A suspicious mind is a healthy mind
|
||||||
|
Frogs are my favorite vegetable.
|
||||||
|
GET OFF THE INTERNET AND DO SOMETHING
|
||||||
|
GET OVER IT
|
||||||
|
Get paranoid to act fast.
|
||||||
|
Get ready for this.
|
||||||
|
Good luck on your exam.
|
||||||
|
Go places.
|
||||||
|
Go talk to that old weird guy at Dennys.
|
||||||
|
Government Complaints Box 📥
|
||||||
|
Happiness is a new idea.
|
||||||
|
Has to happen to somebody.
|
||||||
|
The poors are at it again.
|
||||||
|
Have a nice day. Or else
|
||||||
|
History is written by the winners
|
||||||
|
I can see you
|
||||||
|
ice cream is never the wrong decision
|
||||||
|
I found Nirvana. It was in my album collection.
|
||||||
|
If seeing is believing you have to be blind.
|
||||||
|
I saw what you did just now
|
||||||
|
It is forbidden to forbid.
|
||||||
|
It is happening again.
|
||||||
|
Knock yourself out.
|
||||||
|
Knowing is hard.
|
||||||
|
knowing yourself lets you understand others
|
||||||
|
knowledge should be advanced at all costs
|
||||||
|
Look for people picking their nose in their car.
|
||||||
|
Looks like someone divided by zero.
|
||||||
|
Look up.
|
||||||
|
MAKE IT STOP
|
||||||
|
Make weirdness normal.
|
||||||
|
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.
|
96
message_processing.py
Normal file
96
message_processing.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
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 db_operations import add_bulletin, add_mail, delete_bulletin, delete_mail
|
||||||
|
from utils import get_user_state, get_node_short_name, get_node_id_from_num, send_message
|
||||||
|
|
||||||
|
command_handlers = {
|
||||||
|
"m": handle_mail_command,
|
||||||
|
"b": handle_bulletin_command,
|
||||||
|
"s": handle_stats_command,
|
||||||
|
"f": handle_fortune_command,
|
||||||
|
"w": handle_wall_of_shame_command,
|
||||||
|
"exit": handle_exit_command,
|
||||||
|
"h": handle_help_command,
|
||||||
|
"c": handle_channel_directory_command
|
||||||
|
}
|
||||||
|
|
||||||
|
def process_message(sender_id, message, interface, is_sync_message=False):
|
||||||
|
state = get_user_state(sender_id)
|
||||||
|
message_lower = message.lower()
|
||||||
|
bbs_nodes = interface.bbs_nodes
|
||||||
|
|
||||||
|
if is_sync_message:
|
||||||
|
if message.startswith("BULLETIN|"):
|
||||||
|
parts = message.split("|")
|
||||||
|
board, sender_short_name, subject, content, unique_id = parts[1], parts[2], parts[3], parts[4], parts[5]
|
||||||
|
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)
|
||||||
|
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]
|
||||||
|
add_mail(sender_id, sender_short_name, recipient_id, subject, content, [], interface, unique_id=unique_id)
|
||||||
|
elif message.startswith("DELETE_BULLETIN|"):
|
||||||
|
unique_id = message.split("|")[1]
|
||||||
|
delete_bulletin(unique_id, [], interface)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
handle_help_command(sender_id, interface)
|
||||||
|
|
||||||
|
def on_receive(packet, interface):
|
||||||
|
try:
|
||||||
|
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||||
|
message_bytes = packet['decoded']['payload']
|
||||||
|
message_string = message_bytes.decode('utf-8')
|
||||||
|
sender_id = packet['from']
|
||||||
|
to_id = packet.get('to')
|
||||||
|
sender_node_id = packet['fromId']
|
||||||
|
|
||||||
|
sender_short_name = get_node_short_name(sender_node_id, interface)
|
||||||
|
receiver_short_name = get_node_short_name(get_node_id_from_num(to_id, interface),
|
||||||
|
interface) if to_id else "Group Chat"
|
||||||
|
logging.info(f"Received message from user '{sender_short_name}' to {receiver_short_name}: {message_string}")
|
||||||
|
|
||||||
|
bbs_nodes = interface.bbs_nodes
|
||||||
|
is_sync_message = any(message_string.startswith(prefix) for prefix in
|
||||||
|
["BULLETIN|", "MAIL|", "DELETE_BULLETIN|", "DELETE_MAIL|"])
|
||||||
|
|
||||||
|
if sender_node_id in bbs_nodes:
|
||||||
|
if is_sync_message:
|
||||||
|
process_message(sender_id, message_string, interface, is_sync_message=True)
|
||||||
|
else:
|
||||||
|
logging.info("Ignoring non-sync message from known BBS node")
|
||||||
|
elif to_id is not None and to_id != 0 and to_id != 255 and to_id == interface.myInfo.my_node_num:
|
||||||
|
process_message(sender_id, message_string, interface, is_sync_message=False)
|
||||||
|
else:
|
||||||
|
logging.info("Ignoring message sent to group chat or from unknown node")
|
||||||
|
except KeyError as e:
|
||||||
|
logging.error(f"Error processing packet: {e}")
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
meshtastic
|
||||||
|
pypubsub
|
60
server.py
Normal file
60
server.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
TC²-BBS Server for Meshtastic by TheCommsChannel (TC²)
|
||||||
|
Date: 06/25/2024
|
||||||
|
Version: 0.1.0
|
||||||
|
|
||||||
|
Description:
|
||||||
|
The system allows for mail message handling, bulletin boards, and a channel
|
||||||
|
directory. It uses a configuration file for setup details and an SQLite3
|
||||||
|
database for data storage. Mail messages and bulletins are synced with
|
||||||
|
other BBS servers listed in the config.ini file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from config_init import initialize_config, get_interface
|
||||||
|
from db_operations import initialize_database
|
||||||
|
from message_processing import on_receive
|
||||||
|
from pubsub import pub
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
def display_banner():
|
||||||
|
banner = """
|
||||||
|
████████╗ ██████╗██████╗ ██████╗ ██████╗ ███████╗
|
||||||
|
╚══██╔══╝██╔════╝╚════██╗ ██╔══██╗██╔══██╗██╔════╝
|
||||||
|
██║ ██║ █████╔╝█████╗██████╔╝██████╔╝███████╗
|
||||||
|
██║ ██║ ██╔═══╝ ╚════╝██╔══██╗██╔══██╗╚════██║
|
||||||
|
██║ ╚██████╗███████╗ ██████╔╝██████╔╝███████║
|
||||||
|
╚═╝ ╚═════╝╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
||||||
|
Meshtastic Version
|
||||||
|
"""
|
||||||
|
print(banner)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
display_banner()
|
||||||
|
config, interface_type, hostname, port, bbs_nodes = initialize_config()
|
||||||
|
interface = get_interface(interface_type, hostname, port)
|
||||||
|
interface.bbs_nodes = bbs_nodes
|
||||||
|
|
||||||
|
logging.info(f"TC²-BBS is running on {interface_type} interface...")
|
||||||
|
|
||||||
|
initialize_database()
|
||||||
|
|
||||||
|
def receive_packet(packet):
|
||||||
|
on_receive(packet, interface)
|
||||||
|
|
||||||
|
pub.subscribe(receive_packet, 'meshtastic.receive')
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
pass
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.info("Shutting down the server...")
|
||||||
|
interface.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
73
utils.py
Normal file
73
utils.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
user_states = {}
|
||||||
|
|
||||||
|
|
||||||
|
def update_user_state(user_id, state):
|
||||||
|
user_states[user_id] = state
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_state(user_id):
|
||||||
|
return user_states.get(user_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
def send_message(message, destination, interface):
|
||||||
|
max_payload_size = 200
|
||||||
|
for i in range(0, len(message), max_payload_size):
|
||||||
|
chunk = message[i:i + max_payload_size]
|
||||||
|
interface.sendText(
|
||||||
|
text=chunk,
|
||||||
|
destinationId=destination,
|
||||||
|
wantAck=False,
|
||||||
|
wantResponse=False
|
||||||
|
)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
|
||||||
|
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]
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def get_node_id_from_num(node_num, interface):
|
||||||
|
for node_id, node in interface.nodes.items():
|
||||||
|
if node['num'] == node_num:
|
||||||
|
return node_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_node_short_name(node_id, interface):
|
||||||
|
node_info = interface.nodes.get(node_id)
|
||||||
|
if node_info:
|
||||||
|
return node_info['user']['shortName']
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def send_bulletin_to_bbs_nodes(board, sender_short_name, subject, content, unique_id, bbs_nodes, interface):
|
||||||
|
message = f"BULLETIN|{board}|{sender_short_name}|{subject}|{content}|{unique_id}"
|
||||||
|
for node_id in bbs_nodes:
|
||||||
|
send_message(message, node_id, interface)
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail_to_bbs_nodes(sender_id, sender_short_name, recipient_id, subject, content, unique_id, bbs_nodes,
|
||||||
|
interface):
|
||||||
|
message = f"MAIL|{sender_id}|{sender_short_name}|{recipient_id}|{subject}|{content}|{unique_id}"
|
||||||
|
logging.info(f"SERVER SYNC: Syncing new mail message {subject} sent from {sender_short_name} to other BBS systems.")
|
||||||
|
for node_id in bbs_nodes:
|
||||||
|
send_message(message, node_id, interface)
|
||||||
|
|
||||||
|
|
||||||
|
def send_delete_bulletin_to_bbs_nodes(bulletin_id, bbs_nodes, interface):
|
||||||
|
message = f"DELETE_BULLETIN|{bulletin_id}"
|
||||||
|
for node_id in bbs_nodes:
|
||||||
|
send_message(message, node_id, interface)
|
||||||
|
|
||||||
|
|
||||||
|
def send_delete_mail_to_bbs_nodes(unique_id, bbs_nodes, interface):
|
||||||
|
message = f"DELETE_MAIL|{unique_id}"
|
||||||
|
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)
|
Loading…
Reference in a new issue