refactor to make linter happy
This commit is contained in:
parent
3f27a1bd7a
commit
800d7de809
@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
||||||
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
||||||
@ -17,4 +16,4 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
__all__ = ["bbs", "message", "parser", "project_root"]
|
__all__ = ["rsbbs", "bbs", "message", "parser"]
|
||||||
|
|||||||
369
rsbbs/bbs.py
369
rsbbs/bbs.py
@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
||||||
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
||||||
@ -22,15 +21,14 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import yaml
|
import yaml
|
||||||
|
import platformdirs
|
||||||
|
|
||||||
from sqlalchemy import create_engine, delete, select
|
from sqlalchemy import create_engine, delete, select, or_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from rsbbs.message import Message, Base
|
from rsbbs.message import Message, Base
|
||||||
from rsbbs.parser import Parser
|
from rsbbs.parser import Parser
|
||||||
|
|
||||||
import platformdirs
|
|
||||||
|
|
||||||
|
|
||||||
# Main BBS class
|
# Main BBS class
|
||||||
|
|
||||||
@ -38,14 +36,14 @@ class BBS():
|
|||||||
|
|
||||||
def __init__(self, sysv_args):
|
def __init__(self, sysv_args):
|
||||||
|
|
||||||
self.sysv_args = sysv_args
|
self._sysv_args = sysv_args
|
||||||
|
|
||||||
self.config = self.load_config(sysv_args.config_file)
|
self.config = self._load_config(sysv_args.config_file)
|
||||||
|
|
||||||
self.calling_station = sysv_args.calling_station.upper()
|
self.calling_station = sysv_args.calling_station.upper()
|
||||||
|
|
||||||
self.engine = self.init_engine()
|
self.engine = self._init_engine()
|
||||||
self.parser = self.init_parser()
|
self.parser = self._init_parser()
|
||||||
|
|
||||||
logging.config.dictConfig(self.config['logging'])
|
logging.config.dictConfig(self.config['logging'])
|
||||||
|
|
||||||
@ -57,100 +55,189 @@ class BBS():
|
|||||||
def config(self, value):
|
def config(self, value):
|
||||||
self._config = value
|
self._config = value
|
||||||
|
|
||||||
# Load the config file
|
#
|
||||||
def load_config(self, config_file):
|
# Config file
|
||||||
|
#
|
||||||
|
|
||||||
config_dir = platformdirs.user_config_dir(appname='rsbbs', ensure_exists=True)
|
def _load_config(self, config_file):
|
||||||
|
"""Load configuration file.
|
||||||
config_path = os.path.join(config_dir, 'config.yaml')
|
|
||||||
config_template_path = os.path.join(os.path.dirname(__file__), 'config_default.yaml')
|
|
||||||
|
|
||||||
|
If a config file is specified, attempt to use that. Otherwise, use a
|
||||||
|
file in the location appropriate to the host system. Create the file
|
||||||
|
if it does not exist, using the config_default.yaml as a default.
|
||||||
|
"""
|
||||||
|
# Use either the specified file or a file in a system config location
|
||||||
|
config_path = config_file or os.path.join(
|
||||||
|
platformdirs.user_config_dir(appname='rsbbs', ensure_exists=True),
|
||||||
|
'config.yaml'
|
||||||
|
)
|
||||||
|
# If the file doesn't exist there, create it
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
|
config_template_path = os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
'config_default.yaml')
|
||||||
|
try:
|
||||||
with open(config_template_path, 'r') as f:
|
with open(config_template_path, 'r') as f:
|
||||||
config_template = yaml.load(f, Loader=yaml.FullLoader)
|
config_template = yaml.load(f, Loader=yaml.FullLoader)
|
||||||
with open(config_path, 'w') as f:
|
with open(config_path, 'w') as f:
|
||||||
yaml.dump(config_template, f)
|
yaml.dump(config_template, f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating configuration file: {e}")
|
||||||
|
exit(1)
|
||||||
|
# Load it
|
||||||
try:
|
try:
|
||||||
with open(config_path, 'r') as f:
|
with open(config_path, 'r') as f:
|
||||||
config = yaml.load(f, Loader=yaml.FullLoader)
|
config = yaml.load(f, Loader=yaml.FullLoader)
|
||||||
except yaml.YAMLError as e:
|
except Exception as e:
|
||||||
print(f"Could not load configuration file. Error: {e}")
|
print(f"Error loading configuration file: {e}")
|
||||||
exit(1)
|
|
||||||
except FileNotFoundError as e:
|
|
||||||
print(f'Configuration file full path: {os.path.abspath(config_file)}')
|
|
||||||
print(f"Configuration file {config_file} could not be found. Error: {e}")
|
|
||||||
exit(1)
|
|
||||||
except Exception as msg:
|
|
||||||
print(f"Error while loading configuration file {config_file}. Error: {e}")
|
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
logging.info(f"Configuration file was successfully loaded. File name: {config_file}")
|
logging.info(f"Configuration file was successfully loaded."
|
||||||
|
f"File name: {config_path}")
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
#
|
||||||
|
# BBS command parser
|
||||||
|
#
|
||||||
|
|
||||||
# Set up the BBS command parser
|
def _init_parser(self):
|
||||||
|
"""Define BBS command names, aliases, help messages, action (function),
|
||||||
|
and any arguments.
|
||||||
|
|
||||||
def init_parser(self):
|
This configures argparser's internal help and maps user commands to BBS
|
||||||
|
functions.
|
||||||
|
"""
|
||||||
commands = [
|
commands = [
|
||||||
# (name, aliases, helpmsg, function, {arg: {arg attributes}, ...})
|
# (name,
|
||||||
('bye', ['b', 'q'], 'Sign off and disconnect', self.bye, {}),
|
# aliases,
|
||||||
('delete', ['d', 'k'], 'Delete a message', self.delete,
|
# helpmsg,
|
||||||
{'number': {'help': 'The numeric index of the message to delete'}},
|
# function,
|
||||||
),
|
# {arg:
|
||||||
('deletem', ['dm', 'km'], 'Delete all your messages', self.delete_mine, {}),
|
# {arg attributes},
|
||||||
('help', ['h', '?'], 'Show help', self.help, {}),
|
# ...})
|
||||||
('heard', ['j'], 'Show heard stations log', self.heard, {}),
|
('bye',
|
||||||
('list', ['l'], 'List all messages', self.list, {}),
|
['b', 'q'],
|
||||||
('listm', ['lm'], 'List only messages addressed to you', self.list_mine, {}),
|
'Sign off and disconnect',
|
||||||
('read', ['r'], 'Read messages', self.read,
|
self.bye,
|
||||||
{'number': {'help': 'Message number to read'}}
|
{}),
|
||||||
),
|
|
||||||
('readm', ['rm'], 'Read only messages addressed to you', self.read_mine, {}),
|
('delete',
|
||||||
('send', ['s'], 'Send a new message to a user', self.send,
|
['d', 'k'],
|
||||||
|
'Delete a message',
|
||||||
|
self.delete,
|
||||||
|
{'number':
|
||||||
|
{'help':
|
||||||
|
'The numeric index of the message to delete'}},),
|
||||||
|
|
||||||
|
('deletem',
|
||||||
|
['dm', 'km'],
|
||||||
|
'Delete all your messages',
|
||||||
|
self.delete_mine,
|
||||||
|
{}),
|
||||||
|
|
||||||
|
('help',
|
||||||
|
['h', '?'],
|
||||||
|
'Show help',
|
||||||
|
self.help,
|
||||||
|
{}),
|
||||||
|
|
||||||
|
('heard',
|
||||||
|
['j'],
|
||||||
|
'Show heard stations log',
|
||||||
|
self.heard,
|
||||||
|
{}),
|
||||||
|
|
||||||
|
('list',
|
||||||
|
['l'],
|
||||||
|
'List all messages',
|
||||||
|
self.list,
|
||||||
|
{}),
|
||||||
|
|
||||||
|
('listm',
|
||||||
|
['lm'],
|
||||||
|
'List only messages addressed to you',
|
||||||
|
self.list_mine,
|
||||||
|
{}),
|
||||||
|
|
||||||
|
('read',
|
||||||
|
['r'],
|
||||||
|
'Read messages',
|
||||||
|
self.read,
|
||||||
|
{'number': {'help': 'Message number to read'}}),
|
||||||
|
|
||||||
|
('readm',
|
||||||
|
['rm'],
|
||||||
|
'Read only messages addressed to you',
|
||||||
|
self.read_mine,
|
||||||
|
{}),
|
||||||
|
|
||||||
|
('send',
|
||||||
|
['s'],
|
||||||
|
'Send a new message to a user',
|
||||||
|
self.send,
|
||||||
{
|
{
|
||||||
'callsign': {'help': 'Message recipient callsign'},
|
'callsign': {'help': 'Message recipient callsign'},
|
||||||
'--subject': {'help': 'Message subject'},
|
'--subject': {'help': 'Message subject'},
|
||||||
'--message': {'help': 'Message'},
|
'--message': {'help': 'Message'},
|
||||||
},
|
},),
|
||||||
),
|
|
||||||
('sendp', ['sp'], 'Send a private message to a user', self.send_private,
|
('sendp',
|
||||||
|
['sp'],
|
||||||
|
'Send a private message to a user',
|
||||||
|
self.send_private,
|
||||||
{
|
{
|
||||||
'callsign': {'help': 'Message recipient callsign'},
|
'callsign': {'help': 'Message recipient callsign'},
|
||||||
'--subject': {'help': 'Message subject'},
|
'--subject': {'help': 'Message subject'},
|
||||||
'--message': {'help': 'Message'},
|
'--message': {'help': 'Message'},
|
||||||
},
|
},),]
|
||||||
),
|
|
||||||
]
|
# Send all the commands defined above to the parser
|
||||||
return Parser(commands).parser
|
return Parser(commands).parser
|
||||||
|
|
||||||
|
#
|
||||||
# Database
|
# Database
|
||||||
|
#
|
||||||
|
|
||||||
def init_engine(self):
|
def _init_engine(self):
|
||||||
db_path = os.path.join(platformdirs.user_data_dir(appname='rsbbs', ensure_exists=True), 'messages.db')
|
"""Create a connection to the sqlite3 database.
|
||||||
engine = create_engine('sqlite:///' + db_path, echo=self.sysv_args.debug)
|
|
||||||
|
The default location is the system-specific user-level data directory.
|
||||||
|
"""
|
||||||
|
db_path = os.path.join(
|
||||||
|
platformdirs.user_data_dir(appname='rsbbs', ensure_exists=True),
|
||||||
|
'messages.db')
|
||||||
|
engine = create_engine(
|
||||||
|
'sqlite:///' + db_path,
|
||||||
|
echo=self._sysv_args.debug) # Echo SQL output if -d is turned on
|
||||||
|
# Create the database schema if none exists
|
||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
return engine
|
return engine
|
||||||
|
|
||||||
|
#
|
||||||
# Input and output
|
# Input and output
|
||||||
|
#
|
||||||
|
|
||||||
def read_line(self, prompt):
|
def _read_line(self, prompt):
|
||||||
|
"""Read a single line of input, with an optional prompt,
|
||||||
|
until we get something.
|
||||||
|
"""
|
||||||
output = None
|
output = None
|
||||||
while output == None:
|
while not output:
|
||||||
if prompt:
|
if prompt:
|
||||||
self.write_output(prompt)
|
self._write_output(prompt)
|
||||||
input = sys.stdin.readline().strip()
|
input = sys.stdin.readline().strip()
|
||||||
if input != "":
|
if input != "":
|
||||||
output = input
|
output = input
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def read_multiline(self, prompt):
|
def _read_multiline(self, prompt):
|
||||||
|
"""Read multiple lines of input, with an optional prompt,
|
||||||
|
until the user enters '/ex' by itself on a line.
|
||||||
|
"""
|
||||||
output = []
|
output = []
|
||||||
if prompt:
|
if prompt:
|
||||||
self.write_output(prompt)
|
self._write_output(prompt)
|
||||||
while True:
|
while True:
|
||||||
line = sys.stdin.readline()
|
line = sys.stdin.readline()
|
||||||
if line.lower().strip() == "/ex":
|
if line.lower().strip() == "/ex":
|
||||||
@ -159,118 +246,166 @@ class BBS():
|
|||||||
output.append(line)
|
output.append(line)
|
||||||
return ''.join(output)
|
return ''.join(output)
|
||||||
|
|
||||||
def write_output(self, output):
|
def _write_output(self, output):
|
||||||
|
"""Write something to stdout."""
|
||||||
sys.stdout.write(output + '\r\n')
|
sys.stdout.write(output + '\r\n')
|
||||||
|
|
||||||
def print_message_list(self, results):
|
def print_message_list(self, messages):
|
||||||
self.write_output(f"{'MSG#': <{5}} {'TO': <{9}} {'FROM': <{9}} {'DATE': <{10}} SUBJECT")
|
"""Print a list of messages."""
|
||||||
for result in results:
|
# Print the column headers
|
||||||
self.write_output(f"{result.Message.id: <{5}} {result.Message.recipient: <{9}} {result.Message.sender: <{9}} {result.Message.datetime.strftime('%Y-%m-%d')} {result.Message.subject}")
|
self._write_output(f"{'MSG#': <{5}} "
|
||||||
|
f"{'TO': <{9}} "
|
||||||
|
f"{'FROM': <{9}} "
|
||||||
|
f"{'DATE': <{11}} "
|
||||||
|
f"SUBJECT")
|
||||||
|
# Print the messages
|
||||||
|
for message in messages:
|
||||||
|
datetime_ = message.Message.datetime.strftime('%Y-%m-%d')
|
||||||
|
self._write_output(f"{message.Message.id: <{5}} "
|
||||||
|
f"{message.Message.recipient: <{9}} "
|
||||||
|
f"{message.Message.sender: <{9}} "
|
||||||
|
f"{datetime_: <{11}} "
|
||||||
|
f"{message.Message.subject}")
|
||||||
|
|
||||||
def print_message(self, message):
|
def print_message(self, message):
|
||||||
self.write_output(f"")
|
"""Print an individual message."""
|
||||||
self.write_output(f"Message: {message.Message.id}")
|
# Format the big ol' date and time string
|
||||||
self.write_output(f"Date: {message.Message.datetime.strftime('%A, %B %-d, %Y at %-H:%M %p UTC')}")
|
datetime = message.Message.datetime.strftime(
|
||||||
# self.write_output(f"Date: {message.Message.datetime.strftime('%Y-%m-%dT%H:%M:%S+0000')}")
|
'%A, %B %-d, %Y at %-H:%M %p UTC')
|
||||||
self.write_output(f"From: {message.Message.sender}")
|
# Print the message
|
||||||
self.write_output(f"To: {message.Message.recipient}")
|
self._write_output(f"")
|
||||||
self.write_output(f"Subject: {message.Message.subject}")
|
self._write_output(f"Message: {message.Message.id}")
|
||||||
self.write_output(f"\r\n{message.Message.message}")
|
self._write_output(f"Date: {datetime}")
|
||||||
|
self._write_output(f"From: {message.Message.sender}")
|
||||||
|
self._write_output(f"To: {message.Message.recipient}")
|
||||||
|
self._write_output(f"Subject: {message.Message.subject}")
|
||||||
|
self._write_output(f"")
|
||||||
|
self._write_output(f"{message.Message.message}")
|
||||||
|
|
||||||
|
#
|
||||||
# BBS command functions
|
# BBS command functions
|
||||||
|
#
|
||||||
|
|
||||||
def bye(self, args):
|
def bye(self, args):
|
||||||
'''Disconnect and exit'''
|
"""Close the connection and exit."""
|
||||||
self.write_output("Bye!")
|
self._write_output("Bye!")
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
def delete(self, args):
|
def delete(self, args):
|
||||||
'''Delete message specified by numeric index'''
|
"""Delete a message.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
number -- the message number to delete
|
||||||
|
"""
|
||||||
with Session(self.engine) as session:
|
with Session(self.engine) as session:
|
||||||
try:
|
try:
|
||||||
message = session.get(Message, args.number)
|
message = session.get(Message, args.number)
|
||||||
session.delete(message)
|
session.delete(message)
|
||||||
session.commit()
|
session.commit()
|
||||||
self.write_output(f"Deleted message #{args.number}")
|
self._write_output(f"Deleted message #{args.number}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.write_output(f"Unable to delete message #{args.number}")
|
self._write_output(f"Unable to delete message #{args.number}")
|
||||||
|
|
||||||
def delete_mine(self, args):
|
def delete_mine(self, args):
|
||||||
'''Delete all messages addressed to user'''
|
"""Delete all messages addressed to the calling station's callsign."""
|
||||||
self.write_output("Delete all messages addressed to you? Y/N:")
|
self._write_output("Delete all messages addressed to you? Y/N:")
|
||||||
response = sys.stdin.readline().strip()
|
response = sys.stdin.readline().strip()
|
||||||
if response.lower() != "y":
|
if response.lower() != "y":
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
with Session(self.engine) as session:
|
with Session(self.engine) as session:
|
||||||
try:
|
try:
|
||||||
statement = delete(Message).where(Message.recipient == self.calling_station).returning(Message)
|
statement = delete(Message).where(
|
||||||
|
Message.recipient == self.calling_station
|
||||||
|
).returning(Message)
|
||||||
results = session.execute(statement)
|
results = session.execute(statement)
|
||||||
count = len(results.all())
|
count = len(results.all())
|
||||||
if count > 0:
|
if count > 0:
|
||||||
self.write_output(f"Deleted {count} messages")
|
self._write_output(f"Deleted {count} messages")
|
||||||
session.commit()
|
session.commit()
|
||||||
else:
|
else:
|
||||||
self.write_output(f"No messages to delete.")
|
self._write_output(f"No messages to delete.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.write_output(f"Unable to delete messages: {e}")
|
self._write_output(f"Unable to delete messages: {e}")
|
||||||
|
|
||||||
def heard(self, args):
|
def heard(self, args):
|
||||||
'''Show heard stations log'''
|
"""Show a log of stations that have been heard by this station,
|
||||||
self.write_output(f"Heard stations:")
|
also known as the 'mheard' (linux) or 'jheard' (KPC, etc.) log.
|
||||||
|
"""
|
||||||
|
self._write_output(f"Heard stations:")
|
||||||
result = subprocess.run(['mheard'], capture_output=True, text=True)
|
result = subprocess.run(['mheard'], capture_output=True, text=True)
|
||||||
self.write_output(result.stdout)
|
self._write_output(result.stdout)
|
||||||
|
|
||||||
def help(self, args):
|
def help(self, args):
|
||||||
'''Print help'''
|
"""Show help."""
|
||||||
self.parser.print_help()
|
self.parser.print_help()
|
||||||
|
|
||||||
def list(self, args):
|
def list(self, args):
|
||||||
'''List all messages'''
|
"""List all messages."""
|
||||||
with Session(self.engine) as session:
|
with Session(self.engine) as session:
|
||||||
statement = select(Message).where((Message.is_private == False) | (Message.recipient == self.calling_station))
|
statement = select(Message).where(or_(
|
||||||
|
(Message.is_private.is_not(False)),
|
||||||
|
(Message.recipient.__eq__(self.calling_station)))
|
||||||
|
)
|
||||||
results = session.execute(statement)
|
results = session.execute(statement)
|
||||||
self.print_message_list(results)
|
self.print_message_list(results)
|
||||||
|
|
||||||
def list_mine(self, args):
|
def list_mine(self, args):
|
||||||
'''List only messages addressed to user'''
|
"""List only messages addressed to the calling station's callsign,
|
||||||
|
including public and private messages.
|
||||||
|
"""
|
||||||
with Session(self.engine) as session:
|
with Session(self.engine) as session:
|
||||||
statement = select(Message).where(Message.recipient == self.calling_station)
|
statement = select(Message).where(
|
||||||
|
Message.recipient == self.calling_station)
|
||||||
results = session.execute(statement)
|
results = session.execute(statement)
|
||||||
self.print_message_list(results)
|
self.print_message_list(results)
|
||||||
|
|
||||||
def read(self, args):
|
def read(self, args):
|
||||||
'''Read messages'''
|
"""Read a message.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
number -- the message number to read
|
||||||
|
"""
|
||||||
with Session(self.engine) as session:
|
with Session(self.engine) as session:
|
||||||
statement = select(Message).where(Message.id == args.number)
|
statement = select(Message).where(Message.id == args.number)
|
||||||
result = session.execute(statement).one()
|
result = session.execute(statement).one()
|
||||||
self.print_message(result)
|
self.print_message(result)
|
||||||
|
|
||||||
def read_mine(self, args):
|
def read_mine(self, args):
|
||||||
'''Read only messages addressed to user'''
|
"""Read all messages addressed to the calling station's callsign,
|
||||||
|
in sequence."""
|
||||||
with Session(self.engine) as session:
|
with Session(self.engine) as session:
|
||||||
statement = select(Message).where(Message.recipient == self.calling_station)
|
statement = select(Message).where(
|
||||||
|
Message.recipient == self.calling_station)
|
||||||
result = session.execute(statement)
|
result = session.execute(statement)
|
||||||
messages = result.all()
|
messages = result.all()
|
||||||
count = len(messages)
|
count = len(messages)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
self.write_output(f"Reading {count} messages:")
|
self._write_output(f"Reading {count} messages:")
|
||||||
for message in messages:
|
for message in messages:
|
||||||
self.print_message(message)
|
self.print_message(message)
|
||||||
self.write_output("Enter to continue...")
|
self._write_output("Enter to continue...")
|
||||||
sys.stdin.readline()
|
sys.stdin.readline()
|
||||||
else:
|
else:
|
||||||
self.write_output(f"No messages to read.")
|
self._write_output(f"No messages to read.")
|
||||||
|
|
||||||
def send(self, args, is_private=False):
|
def send(self, args, is_private=False):
|
||||||
'''Create a message addressed to another user'''
|
"""Create a new message addressed to another callsign.
|
||||||
|
|
||||||
|
Required arguments:
|
||||||
|
callsign -- the recipient's callsign
|
||||||
|
|
||||||
|
Optional arguments:
|
||||||
|
subject -- message subject
|
||||||
|
message -- the message itself
|
||||||
|
"""
|
||||||
if not args.callsign:
|
if not args.callsign:
|
||||||
args.callsign = self.read_line("Callsign:")
|
args.callsign = self._read_line("Callsign:")
|
||||||
if not args.subject:
|
if not args.subject:
|
||||||
args.subject = self.read_line("Subject:")
|
args.subject = self._read_line("Subject:")
|
||||||
if not args.message:
|
if not args.message:
|
||||||
args.message = self.read_multiline("Message - end with /ex on a single line:")
|
args.message = self._read_multiline(
|
||||||
|
"Message - end with /ex on a single line:")
|
||||||
with Session(self.engine) as session:
|
with Session(self.engine) as session:
|
||||||
session.add(Message(
|
session.add(Message(
|
||||||
sender=self.calling_station.upper(),
|
sender=self.calling_station.upper(),
|
||||||
@ -281,25 +416,41 @@ class BBS():
|
|||||||
))
|
))
|
||||||
try:
|
try:
|
||||||
session.commit()
|
session.commit()
|
||||||
self.write_output("Message saved!")
|
self._write_output("Message saved!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
self.write_output("Error saving message. Contact the sysop for assistance.")
|
self._write_output("Error saving message."
|
||||||
|
"Contact the sysop for assistance.")
|
||||||
|
|
||||||
def send_private(self, args):
|
def send_private(self, args):
|
||||||
self.send(args, is_private=True)
|
self.send(args, is_private=True)
|
||||||
|
"""Send a message visible only to the recipient callsign.
|
||||||
|
|
||||||
|
Required arguments:
|
||||||
|
callsign -- the recipient's callsign
|
||||||
|
|
||||||
|
Optional arguments:
|
||||||
|
subject -- message subject
|
||||||
|
message -- the message itself
|
||||||
|
"""
|
||||||
|
|
||||||
|
#
|
||||||
# Main loop
|
# Main loop
|
||||||
|
#
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# Show greeting
|
# Show greeting
|
||||||
self.write_output(f"[RSBBS-1.0.0] listening on {self.config['callsign']} ")
|
greeting = []
|
||||||
self.write_output(f"Welcome to {self.config['bbs_name']}, {self.calling_station}")
|
greeting.append(f"[RSBBS-1.0.0] listening on "
|
||||||
self.write_output(self.config['banner_message'])
|
f"{self.config['callsign']}")
|
||||||
self.write_output("For help, enter 'h'")
|
greeting.append(f"Welcome to {self.config['bbs_name']}, "
|
||||||
|
f"{self.calling_station}")
|
||||||
|
greeting.append(self.config['banner_message'])
|
||||||
|
greeting.append("For help, enter 'h'")
|
||||||
|
self._write_output('\r\n'.join(greeting))
|
||||||
|
|
||||||
# Show initial prompt to the calling user
|
# Show initial prompt to the calling user
|
||||||
self.write_output(self.config['command_prompt'])
|
self._write_output(self.config['command_prompt'])
|
||||||
|
|
||||||
# Parse the BBS interactive commands for the rest of time
|
# Parse the BBS interactive commands for the rest of time
|
||||||
for line in sys.stdin:
|
for line in sys.stdin:
|
||||||
@ -310,4 +461,4 @@ class BBS():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Show our prompt to the calling user again
|
# Show our prompt to the calling user again
|
||||||
self.write_output(self.config['command_prompt'])
|
self._write_output(self.config['command_prompt'])
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
||||||
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
||||||
@ -22,9 +21,11 @@ from datetime import datetime, timezone
|
|||||||
from sqlalchemy import Boolean, DateTime, String
|
from sqlalchemy import Boolean, DateTime, String
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Message(Base):
|
class Message(Base):
|
||||||
__tablename__ = 'message'
|
__tablename__ = 'message'
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
@ -32,6 +33,6 @@ class Message(Base):
|
|||||||
recipient: Mapped[str] = mapped_column(String)
|
recipient: Mapped[str] = mapped_column(String)
|
||||||
subject: Mapped[str] = mapped_column(String)
|
subject: Mapped[str] = mapped_column(String)
|
||||||
message: Mapped[str] = mapped_column(String)
|
message: Mapped[str] = mapped_column(String)
|
||||||
datetime: Mapped[DateTime] = mapped_column(DateTime, default=datetime.now(timezone.utc))
|
datetime: Mapped[DateTime] = mapped_column(
|
||||||
|
DateTime, default=datetime.now(timezone.utc))
|
||||||
is_private: Mapped[bool] = mapped_column(Boolean)
|
is_private: Mapped[bool] = mapped_column(Boolean)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
||||||
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
||||||
@ -19,6 +18,7 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
# We want to override the error and exit methods of ArgumentParser
|
# We want to override the error and exit methods of ArgumentParser
|
||||||
# to prevent it exiting unexpectedly or spewing error data over the air
|
# to prevent it exiting unexpectedly or spewing error data over the air
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
|
||||||
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
'''
|
|
||||||
This is a placeholder to be able to access the location of project root directory.
|
|
||||||
This is needed to be able to read config.yaml from this package, when it is distributed with setuptools and installed.
|
|
||||||
'''
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
def path():
|
|
||||||
return os.path.dirname(__file__)
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
||||||
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
||||||
@ -22,6 +21,7 @@ import sys
|
|||||||
|
|
||||||
from rsbbs.bbs import BBS
|
from rsbbs.bbs import BBS
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
# Parse and handle the system invocation arguments
|
# Parse and handle the system invocation arguments
|
||||||
@ -31,12 +31,17 @@ def main():
|
|||||||
# Configure args:
|
# Configure args:
|
||||||
args_list = [
|
args_list = [
|
||||||
# [ short, long, action, default, dest, help, required ]
|
# [ short, long, action, default, dest, help, required ]
|
||||||
['-d', '--debug', 'store_true', None, 'debug', 'Enable debugging output to stdout', False],
|
['-d', '--debug', 'store_true', None, 'debug',
|
||||||
['-s', '--calling-station', 'store', 'N0CALL', 'calling_station', 'The callsign of the calling station', True],
|
'Enable debugging output to stdout', False],
|
||||||
['-f', '--config-file', 'store', 'config.yaml', 'config_file', 'specify path to config.yaml file', False],
|
['-s', '--calling-station', 'store', 'N0CALL', 'calling_station',
|
||||||
|
'The callsign of the calling station', True],
|
||||||
|
['-f', '--config-file', 'store', 'config.yaml', 'config_file',
|
||||||
|
'specify path to config.yaml file', False],
|
||||||
]
|
]
|
||||||
for arg in args_list:
|
for arg in args_list:
|
||||||
sysv_parser.add_argument(arg[0], arg[1], action=arg[2], default=arg[3], dest=arg[4], help=arg[5], required=arg[6])
|
sysv_parser.add_argument(
|
||||||
|
arg[0], arg[1], action=arg[2], default=arg[3], dest=arg[4],
|
||||||
|
help=arg[5], required=arg[6])
|
||||||
|
|
||||||
# Version arg is special:
|
# Version arg is special:
|
||||||
sysv_parser.add_argument('-v', '--version',
|
sysv_parser.add_argument('-v', '--version',
|
||||||
@ -52,5 +57,6 @@ def main():
|
|||||||
# Start the main BBS loop
|
# Start the main BBS loop
|
||||||
bbs.run()
|
bbs.run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
1
setup.py
1
setup.py
@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
# Really Simple BBS - a really simple BBS for ax.25 packet radio.
|
||||||
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
# Copyright (C) 2023 John Burwell <john@atatdotdot.com>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user