Add files via upload

This commit is contained in:
TC² 2024-06-25 08:50:52 -04:00 committed by GitHub
parent 36ac328636
commit 339210401f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1054 additions and 0 deletions

109
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
meshtastic
pypubsub

60
server.py Normal file
View 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
View 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)