diff --git a/rsbbs/config.py b/rsbbs/config.py
index 0d6a71d..7170d7b 100644
--- a/rsbbs/config.py
+++ b/rsbbs/config.py
@@ -48,6 +48,7 @@ class Config():
def __getattr__(self, __name: str):
return self.config[__name]
+ # Format the config for display
def __repr__(self):
repr = []
repr.append(f"app_name: {self.app_name}\r\n")
diff --git a/rsbbs/console.py b/rsbbs/console.py
index 9e4644d..f7eb548 100644
--- a/rsbbs/console.py
+++ b/rsbbs/console.py
@@ -16,11 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import os
import sys
-import sqlalchemy.exc
-
import rsbbs
from rsbbs.config import Config
from rsbbs.controller import Controller
@@ -134,22 +131,6 @@ class Console():
f"{datetime_: <{11}} "
f"{message.Message.subject}")
- #
- # Command functions
- #
-
- def send_private(self, args):
- 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 input loop
#
diff --git a/rsbbs/controller.py b/rsbbs/controller.py
index be6e663..8e98e29 100644
--- a/rsbbs/controller.py
+++ b/rsbbs/controller.py
@@ -16,42 +16,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import subprocess
-
-from datetime import datetime, timezone
-
-from sqlalchemy import Boolean, DateTime, String
-from sqlalchemy import create_engine, delete, select, or_
-
-from sqlalchemy.orm import DeclarativeBase, Mapped, Session
-from sqlalchemy.orm import mapped_column
+from sqlalchemy import create_engine
+from sqlalchemy.orm import Session
from rsbbs.config import Config
-
-
-class Base(DeclarativeBase):
- pass
-
-
-class Message(Base):
- __tablename__ = 'message'
- id: Mapped[int] = mapped_column(primary_key=True)
- sender: Mapped[str] = mapped_column(String)
- recipient: Mapped[str] = mapped_column(String)
- subject: Mapped[str] = mapped_column(String)
- message: Mapped[str] = mapped_column(String)
- datetime: Mapped[DateTime] = mapped_column(
- DateTime, default=datetime.now(timezone.utc))
- is_private: Mapped[bool] = mapped_column(Boolean)
+from rsbbs.models import Base
class Controller():
- def __init__(self, config: Config):
+ def __init__(self, config: Config) -> None:
self.config = config
self._init_datastore()
- def _init_datastore(self):
+ def _init_datastore(self) -> None:
"""Create a connection to the sqlite3 database.
The default location is the system-specific user-level data directory.
@@ -64,103 +42,5 @@ class Controller():
# Create the database schema if none exists
Base.metadata.create_all(self.engine)
- def delete(self, args):
- """Delete a message.
-
- Arguments:
- number -- the message number to delete
- """
- with Session(self.engine) as session:
- try:
- message = session.get(Message, args.number)
- session.delete(message)
- session.commit()
- except Exception:
- raise
-
- def delete_mine(self, args):
- """Delete all messages addressed to the calling station's callsign."""
- with Session(self.engine) as session:
- try:
- statement = delete(Message).where(
- Message.recipient == self.config.calling_station
- ).returning(Message)
- result = session.execute(
- statement,
- execution_options={"prebuffer_rows": True})
- session.commit()
- return result
- except Exception:
- raise
-
- def list(self, args):
- """List all messages."""
- with Session(self.engine) as session:
- try:
- # Using or_ and is_ etc. to distinguish from python operators
- statement = select(Message).where(
- or_(
- (Message.is_private.is_(False)),
- (Message.recipient.__eq__(
- self.config.calling_station)))
- )
- result = session.execute(
- statement,
- execution_options={"prebuffer_rows": True})
- except Exception:
- raise
- return result
-
- def list_mine(self, args):
- """List only messages addressed to the calling station's callsign,
- including public and private messages.
- """
- with Session(self.engine) as session:
- try:
- statement = select(Message).where(
- Message.recipient == self.config.calling_station)
- result = session.execute(
- statement,
- execution_options={"prebuffer_rows": True})
- return result
- except Exception:
- raise
-
- def read(self, args):
- """Read a message.
-
- Arguments:
- number -- the message number to read
- """
- with Session(self.engine) as session:
- try:
- statement = select(Message).where(Message.id == args.number)
- result = session.execute(statement).one()
- return result
- except Exception:
- raise
-
- def send(self, args, is_private=False):
- """Create a new message addressed to another callsign.
-
- Required arguments:
- callsign -- the recipient's callsign
-
- Optional arguments:
- subject -- message subject
- message -- the message itself
- """
- with Session(self.engine) as session:
- try:
- session.add(Message(
- sender=self.config.calling_station.upper(),
- recipient=args.callsign.upper(),
- subject=args.subject,
- message=args.message,
- is_private=is_private
- ))
- session.commit()
- return {}
- except Exception:
- session.rollback()
- raise
+ def session(self) -> Session:
+ return Session(self.engine)
diff --git a/rsbbs/models.py b/rsbbs/models.py
new file mode 100644
index 0000000..941c68b
--- /dev/null
+++ b/rsbbs/models.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+#
+# Really Simple BBS - a really simple BBS for ax.25 packet radio.
+# Copyright (C) 2023 John Burwell
+#
+# 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 .
+
+from datetime import datetime, timezone
+
+from sqlalchemy import Boolean, DateTime, String
+
+from sqlalchemy.orm import DeclarativeBase, Mapped
+from sqlalchemy.orm import mapped_column
+
+
+class Base(DeclarativeBase):
+ pass
+
+
+class Message(Base):
+ __tablename__ = 'message'
+ id: Mapped[int] = mapped_column(primary_key=True)
+ sender: Mapped[str] = mapped_column(String)
+ recipient: Mapped[str] = mapped_column(String)
+ subject: Mapped[str] = mapped_column(String)
+ message: Mapped[str] = mapped_column(String)
+ datetime: Mapped[DateTime] = mapped_column(
+ DateTime, default=datetime.now(timezone.utc))
+ is_private: Mapped[bool] = mapped_column(Boolean)
diff --git a/rsbbs/plugins/bye/plugin.py b/rsbbs/plugins/bye/plugin.py
index 9bc5a41..e7aab51 100644
--- a/rsbbs/plugins/bye/plugin.py
+++ b/rsbbs/plugins/bye/plugin.py
@@ -22,20 +22,20 @@ from rsbbs.parser import Parser
class Plugin():
- def __init__(self, api: Console):
+ def __init__(self, api: Console) -> None:
self.api = api
self.init_parser(api.parser)
if api.config.debug:
print(f"Plugin {__name__} loaded")
- def init_parser(self, parser: Parser):
+ def init_parser(self, parser: Parser) -> None:
subparser = parser.subparsers.add_parser(
name='bye',
aliases=['b', 'q'],
help='Sign off and disconnect')
subparser.set_defaults(func=self.run)
- def run(self, args):
+ def run(self, args) -> None:
"""Disconnect and exit."""
self.api.write_output("Bye!")
exit(0)
diff --git a/rsbbs/plugins/delete/plugin.py b/rsbbs/plugins/delete/plugin.py
index 1b1e417..d7a3374 100644
--- a/rsbbs/plugins/delete/plugin.py
+++ b/rsbbs/plugins/delete/plugin.py
@@ -16,19 +16,23 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+import sqlalchemy
+import sqlalchemy.exc
+
from rsbbs.console import Console
+from rsbbs.models import Message
from rsbbs.parser import Parser
class Plugin():
- def __init__(self, api: Console):
+ def __init__(self, api: Console) -> None:
self.api = api
self.init_parser(api.parser)
if api.config.debug:
print(f"Plugin {__name__} loaded")
- def init_parser(self, parser: Parser):
+ def init_parser(self, parser: Parser) -> None:
subparser = parser.subparsers.add_parser(
name='delete',
aliases=['d', 'k'],
@@ -37,11 +41,23 @@ class Plugin():
help='The number of the message to delete')
subparser.set_defaults(func=self.run)
- def run(self, args):
- """Delete a message specified by ID number."""
- if args.number:
+ def delete(self, number) -> None:
+ with self.api.controller.session() as session:
try:
- self.api.controller.delete(args)
- self.api.write_output(f"Deleted message #{args.number}")
- except Exception as e:
+ message = session.get(Message, number)
+ session.delete(message)
+ session.commit()
+ self.api.write_output(f"Deleted message #{number}")
+ except sqlalchemy.exc.NoResultFound:
self.api.write_output(f"Message not found.")
+ except Exception as e:
+ print(e)
+
+ def run(self, args) -> None:
+ """Delete a message.
+
+ Arguments:
+ number -- the message number to delete
+ """
+ if args.number:
+ self.delete(args.number)
diff --git a/rsbbs/plugins/deletem/plugin.py b/rsbbs/plugins/deletem/plugin.py
index 0020a6f..964058d 100644
--- a/rsbbs/plugins/deletem/plugin.py
+++ b/rsbbs/plugins/deletem/plugin.py
@@ -16,34 +16,38 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+import sqlalchemy
+
from rsbbs.console import Console
+from rsbbs.models import Message
from rsbbs.parser import Parser
class Plugin():
- def __init__(self, api: Console):
+ def __init__(self, api: Console) -> None:
self.api = api
self.init_parser(api.parser)
if api.config.debug:
print(f"Plugin {__name__} loaded")
- def init_parser(self, parser: Parser):
+ def init_parser(self, parser: Parser) -> None:
subparser = parser.subparsers.add_parser(
name='deletem',
aliases=['dm', 'km'],
help='Delete all messages addressed to you')
subparser.set_defaults(func=self.run)
- def run(self, args):
- """Delete all messages addressed to the calling station's callsign."""
- response = self.api.read_line(
- "Delete all messages addressed to you? Y/N:")
- if response.lower() != "y":
- return
- else:
+ def delete_mine(self) -> None:
+ with self.api.controller.session() as session:
try:
- result = self.api.controller.delete_mine(args)
+ statement = sqlalchemy.delete(Message).where(
+ Message.recipient == self.api.config.calling_station
+ ).returning(Message)
+ result = session.execute(
+ statement,
+ execution_options={"prebuffer_rows": True})
+ session.commit()
messages = result.all()
count = len(messages)
if count > 0:
@@ -52,3 +56,10 @@ class Plugin():
self.api.write_output(f"No messages to delete.")
except Exception as e:
self.api.write_output(f"Unable to delete messages: {e}")
+
+ def run(self, args) -> None:
+ """Delete all messages addressed to the calling station's callsign."""
+ response = self.api.read_line(
+ "Delete all messages addressed to you? Y/N:")
+ if response.lower() == "y":
+ self.delete_mine()
diff --git a/rsbbs/plugins/heard/plugin.py b/rsbbs/plugins/heard/plugin.py
index f7bf783..abc9094 100644
--- a/rsbbs/plugins/heard/plugin.py
+++ b/rsbbs/plugins/heard/plugin.py
@@ -24,20 +24,20 @@ from rsbbs.parser import Parser
class Plugin():
- def __init__(self, api: Console):
+ def __init__(self, api: Console) -> None:
self.api = api
self.init_parser(api.parser)
if api.config.debug:
print(f"Plugin {__name__} loaded")
- def init_parser(self, parser: Parser):
+ def init_parser(self, parser: Parser) -> None:
subparser = parser.subparsers.add_parser(
name='heard',
aliases=['j'],
help='Show heard stations log')
subparser.set_defaults(func=self.run)
- def run(self, args):
+ def run(self, args) -> None:
"""Show a log of stations that have been heard by this station,
also known as the 'mheard' (linux) or 'jheard' (KPC, etc.) log.
"""
diff --git a/rsbbs/plugins/help/plugin.py b/rsbbs/plugins/help/plugin.py
index 6974f0d..266ebc5 100644
--- a/rsbbs/plugins/help/plugin.py
+++ b/rsbbs/plugins/help/plugin.py
@@ -22,20 +22,20 @@ from rsbbs.parser import Parser
class Plugin():
- def __init__(self, api: Console):
+ def __init__(self, api: Console) -> None:
self.api = api
self.init_parser(api.parser)
if api.config.debug:
print(f"Plugin {__name__} loaded")
- def init_parser(self, parser: Parser):
+ def init_parser(self, parser: Parser) -> None:
subparser = parser.subparsers.add_parser(
name='help',
aliases=['h', '?'],
help='Show help')
subparser.set_defaults(func=self.run)
- def run(self, args):
+ def run(self, args) -> None:
"""Show a log of stations that have been heard by this station,
also known as the 'mheard' (linux) or 'jheard' (KPC, etc.) log.
"""
diff --git a/rsbbs/plugins/list/plugin.py b/rsbbs/plugins/list/plugin.py
index 44b9901..efd48c5 100644
--- a/rsbbs/plugins/list/plugin.py
+++ b/rsbbs/plugins/list/plugin.py
@@ -16,26 +16,47 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+import sqlalchemy
+
from rsbbs.console import Console
+from rsbbs.models import Message
from rsbbs.parser import Parser
class Plugin():
- def __init__(self, api: Console):
+ def __init__(self, api: Console) -> None:
self.api = api
self.init_parser(api.parser)
if api.config.debug:
print(f"Plugin {__name__} loaded")
- def init_parser(self, parser: Parser):
+ def init_parser(self, parser: Parser) -> None:
subparser = parser.subparsers.add_parser(
name='list',
aliases=['l'],
help='List all available messages')
subparser.set_defaults(func=self.run)
- def run(self, args):
+ def list(self, args) -> sqlalchemy.ChunkedIteratorResult:
+ """List all messages."""
+ with self.api.controller.session() as session:
+ try:
+ # Using or_ and is_ etc. to distinguish from python operators
+ statement = sqlalchemy.select(Message).where(
+ sqlalchemy.or_(
+ (Message.is_private.is_(False)),
+ (Message.recipient.__eq__(
+ self.api.config.calling_station)))
+ )
+ result = session.execute(
+ statement,
+ execution_options={"prebuffer_rows": True})
+ except Exception:
+ raise
+ return result
+
+ def run(self, args) -> None:
"""List all public messages and messages private to the caller."""
- result = self.api.controller.list(args)
+ result = self.list(args)
self.api.print_message_list(result)
diff --git a/rsbbs/plugins/listm/plugin.py b/rsbbs/plugins/listm/plugin.py
index 321b354..40578e1 100644
--- a/rsbbs/plugins/listm/plugin.py
+++ b/rsbbs/plugins/listm/plugin.py
@@ -16,28 +16,43 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+import sqlalchemy
+
from rsbbs.console import Console
+from rsbbs.models import Message
from rsbbs.parser import Parser
class Plugin():
- def __init__(self, api: Console):
+ def __init__(self, api: Console) -> None:
self.api = api
self.init_parser(api.parser)
if api.config.debug:
print(f"Plugin {__name__} loaded")
- def init_parser(self, parser: Parser):
+ def init_parser(self, parser: Parser) -> None:
subparser = parser.subparsers.add_parser(
name='listm',
aliases=['lm'],
help='List only messages addressed to you')
subparser.set_defaults(func=self.run)
+ def list_mine(self, args) -> sqlalchemy.ChunkedIteratorResult:
+ with self.api.controller.session() as session:
+ try:
+ statement = sqlalchemy.select(Message).where(
+ Message.recipient == self.api.config.calling_station)
+ result = session.execute(
+ statement,
+ execution_options={"prebuffer_rows": True})
+ return result
+ except Exception:
+ raise
+
def run(self, args):
"""List only messages addressed to the calling station's callsign,
including public and private messages.
"""
- result = self.api.controller.list_mine(args)
+ result = self.list_mine(args)
self.api.print_message_list(result)
diff --git a/rsbbs/plugins/read/plugin.py b/rsbbs/plugins/read/plugin.py
index 9d88aaf..e88160e 100644
--- a/rsbbs/plugins/read/plugin.py
+++ b/rsbbs/plugins/read/plugin.py
@@ -17,20 +17,22 @@
# along with this program. If not, see .
import sqlalchemy
+import sqlalchemy.exc
from rsbbs.console import Console
+from rsbbs.models import Message
from rsbbs.parser import Parser
class Plugin():
- def __init__(self, api: Console):
+ def __init__(self, api: Console) -> None:
self.api = api
self.init_parser(api.parser)
if api.config.debug:
print(f"Plugin {__name__} loaded")
- def init_parser(self, parser: Parser):
+ def init_parser(self, parser: Parser) -> None:
subparser = parser.subparsers.add_parser(
name='read',
aliases=['r'],
@@ -38,17 +40,23 @@ class Plugin():
subparser.add_argument('number', help='Message number to read')
subparser.set_defaults(func=self.run)
- def run(self, args):
+ def read_message(self, number) -> None:
+ with self.api.controller.session() as session:
+ try:
+ statement = sqlalchemy.select(Message).where(
+ Message.id == number)
+ result = session.execute(statement).one()
+ self.api.print_message(result)
+ except sqlalchemy.exc.NoResultFound:
+ self.api.write_output(f"Message not found.")
+ except Exception as e:
+ print(e)
+
+ def run(self, args) -> None:
"""Read a message.
Arguments:
number -- the message number to read
"""
if args.number:
- try:
- result = self.api.controller.read(args)
- self.api.print_message(result)
- except sqlalchemy.exc.NoResultFound:
- self.api.write_output(f"Message not found.")
- except Exception as e:
- print(e)
+ self.read_message(args.number)
diff --git a/rsbbs/plugins/readm/plugin.py b/rsbbs/plugins/readm/plugin.py
index 292c9a9..27e2e24 100644
--- a/rsbbs/plugins/readm/plugin.py
+++ b/rsbbs/plugins/readm/plugin.py
@@ -19,28 +19,41 @@
import sqlalchemy
from rsbbs.console import Console
+from rsbbs.models import Message
from rsbbs.parser import Parser
class Plugin():
- def __init__(self, api: Console):
+ def __init__(self, api: Console) -> None:
self.api = api
self.init_parser(api.parser)
if api.config.debug:
print(f"Plugin {__name__} loaded")
- def init_parser(self, parser: Parser):
+ def init_parser(self, parser: Parser) -> None:
subparser = parser.subparsers.add_parser(
name='readm',
aliases=['rm'],
help='Read all messages addressed to you')
subparser.set_defaults(func=self.run)
- def run(self, args):
+ def list_mine(self, args) -> sqlalchemy.ChunkedIteratorResult:
+ with self.api.controller.session() as session:
+ try:
+ statement = sqlalchemy.select(Message).where(
+ Message.recipient == self.api.config.calling_station)
+ result = session.execute(
+ statement,
+ execution_options={"prebuffer_rows": True})
+ return result
+ except Exception:
+ raise
+
+ def run(self, args) -> None:
"""Read all messages addressed to the calling station's callsign,
in sequence."""
- result = self.api.controller.list_mine(args)
+ result = self.list_mine(args)
messages = result.all()
count = len(messages)
if count > 0:
diff --git a/rsbbs/plugins/send/plugin.py b/rsbbs/plugins/send/plugin.py
index 2acd1e3..f187800 100644
--- a/rsbbs/plugins/send/plugin.py
+++ b/rsbbs/plugins/send/plugin.py
@@ -16,21 +16,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import sqlalchemy
-
from rsbbs.console import Console
+from rsbbs.models import Message
from rsbbs.parser import Parser
class Plugin():
- def __init__(self, api: Console):
+ def __init__(self, api: Console) -> None:
self.api = api
self.init_parser(api.parser)
if api.config.debug:
print(f"Plugin {__name__} loaded")
- def init_parser(self, parser: Parser):
+ def init_parser(self, parser: Parser) -> None:
subparser = parser.subparsers.add_parser(
name='send',
aliases=['s'],
@@ -40,7 +39,22 @@ class Plugin():
subparser.add_argument('--message', help='Message')
subparser.set_defaults(func=self.run)
- def run(self, args):
+ def send(self, args, is_private=False) -> None:
+ with self.api.controller.session() as session:
+ try:
+ session.add(Message(
+ sender=self.api.config.calling_station.upper(),
+ recipient=args.callsign.upper(),
+ subject=args.subject,
+ message=args.message,
+ is_private=is_private
+ ))
+ session.commit()
+ except Exception:
+ session.rollback()
+ raise
+
+ def run(self, args) -> None:
"""Create a new message addressed to another callsign.
Required arguments:
@@ -58,6 +72,6 @@ class Plugin():
args.message = self.api.read_multiline(
"Message - end with /ex on a single line:")
try:
- self.api.controller.send(args, is_private=False)
+ self.send(args)
except Exception as e:
print(e)
diff --git a/rsbbs/plugins/sendp/plugin.py b/rsbbs/plugins/sendp/plugin.py
index f036e61..57a3b8a 100644
--- a/rsbbs/plugins/sendp/plugin.py
+++ b/rsbbs/plugins/sendp/plugin.py
@@ -16,21 +16,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import sqlalchemy
-
from rsbbs.console import Console
+from rsbbs.models import Message
from rsbbs.parser import Parser
class Plugin():
- def __init__(self, api: Console):
+ def __init__(self, api: Console) -> None:
self.api = api
self.init_parser(api.parser)
if api.config.debug:
print(f"Plugin {__name__} loaded")
- def init_parser(self, parser: Parser):
+ def init_parser(self, parser: Parser) -> None:
subparser = parser.subparsers.add_parser(
name='sendp',
aliases=['sp'],
@@ -40,7 +39,22 @@ class Plugin():
subparser.add_argument('--message', help='Message')
subparser.set_defaults(func=self.run)
- def run(self, args):
+ def send(self, args, is_private=False) -> None:
+ with self.api.controller.session() as session:
+ try:
+ session.add(Message(
+ sender=self.api.config.calling_station.upper(),
+ recipient=args.callsign.upper(),
+ subject=args.subject,
+ message=args.message,
+ is_private=is_private
+ ))
+ session.commit()
+ except Exception:
+ session.rollback()
+ raise
+
+ def run(self, args) -> None:
"""Create a new message addressed to another callsign.
Required arguments:
@@ -58,6 +72,6 @@ class Plugin():
args.message = self.api.read_multiline(
"Message - end with /ex on a single line:")
try:
- self.api.controller.send(args, is_private=True)
+ self.send(args, is_private=True)
except Exception as e:
print(e)
diff --git a/rsbbs/rsbbs.py b/rsbbs/rsbbs.py
index 94e7220..36679fa 100755
--- a/rsbbs/rsbbs.py
+++ b/rsbbs/rsbbs.py
@@ -23,7 +23,6 @@ from rsbbs import __version__
from rsbbs.config import Config
from rsbbs.console import Console
from rsbbs.controller import Controller
-from rsbbs.parser import Parser
def parse_args():