diff --git a/rsbbs/config.py b/rsbbs/config.py index 598e4e6..e40b16b 100644 --- a/rsbbs/config.py +++ b/rsbbs/config.py @@ -36,6 +36,10 @@ class Config(): self.config['debug'] = args.debug self.config['args'] = args + self.config['calling_station'] = args.calling_station.upper() or None + + def __getattr__(self, __name: str): + return self.config[__name] @property def config_path(self): diff --git a/rsbbs/console.py b/rsbbs/console.py index 8d764f0..76f4791 100644 --- a/rsbbs/console.py +++ b/rsbbs/console.py @@ -18,10 +18,12 @@ import sys +import sqlalchemy.exc + import rsbbs from rsbbs.commands import Commands from rsbbs.config import Config -from rsbbs.controller import Base, Controller, Message +from rsbbs.controller import Controller from rsbbs.parser import Parser @@ -40,6 +42,34 @@ class Console(): # Input and output # + def _read_line(self, prompt): + """Read a single line of input, with an optional prompt, + until we get something. + """ + output = None + while not output: + if prompt: + self._write_output(prompt) + input = sys.stdin.readline().strip() + if input != "": + output = input + return output + + 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 = [] + if prompt: + self._write_output(prompt) + while True: + line = sys.stdin.readline() + if line.lower().strip() == "/ex": + break + else: + output.append(line) + return ''.join(output) + def _write_output(self, output): """Write something to stdout.""" sys.stdout.write(output + '\r\n') @@ -48,12 +78,12 @@ class Console(): # Show greeting greeting = [] greeting.append(f"[RSBBS-{rsbbs.__version__}] listening on " - f"{self.config.config['callsign']} ") + f"{self.config.callsign} ") - greeting.append(f"Welcome to {self.config.config['bbs_name']}, " - f"{self.config.config['args'].calling_station}") + greeting.append(f"Welcome to {self.config.bbs_name}, " + f"{self.config.calling_station}") - greeting.append(self.config.config['banner_message']) + greeting.append(self.config.banner_message) greeting.append("For help, enter 'h'") @@ -96,11 +126,18 @@ class Console(): # def bye(self, args): + """Disconnect and exit.""" self._write_output("Bye!") exit(0) def delete(self, args): - self.controller.delete(args) + """Delete a message specified by ID number.""" + if args.number: + try: + self.controller.delete(args) + self._write_output(f"Deleted message #{args.number}") + except Exception as e: + self._write_output(f"Message not found.") def delete_mine(self, args): """Delete all messages addressed to the calling station's callsign.""" @@ -123,16 +160,24 @@ class Console(): also known as the 'mheard' (linux) or 'jheard' (KPC, etc.) log. """ self._write_output(f"Heard stations:") - result = self.controller.heard(args) - self._write_output(result.stdout) + try: + result = self.controller.heard(args) + self._write_output(result.stdout) + except FileNotFoundError: + self._write_output(f"mheard utility not found.") + except Exception as e: + if self.config.debug: + raise + else: + self._write_output(f"Heard stations not available.") def help(self, args): self.parser.parser.print_help() def list(self, args): - """List all messages.""" + """List all public messages and private messages to the caller.""" result = self.controller.list(args) - self.print_message_list(result['result']) + self.print_message_list(result) def list_mine(self, args): """List only messages addressed to the calling station's callsign, @@ -147,16 +192,24 @@ class Console(): Arguments: number -- the message number to read """ - result = self.controller.read(args) - self.print_message(result) + if args.number: + try: + result = self.controller.read(args) + self.print_message(result) + except sqlalchemy.exc.NoResultFound: + self._write_output(f"Message not found.") + except Exception as e: + print(e) def read_mine(self, args): """Read all messages addressed to the calling station's callsign, in sequence.""" result = self.controller.list_mine(args) - if result['count'] > 0: - self._write_output(f"Reading {result['count']} messages:") - for message in result['result'].all(): + messages = result.all() + count = len(messages) + if count > 0: + self._write_output(f"Reading {count} messages:") + for message in messages: self.print_message(message) self._write_output("Enter to continue...") sys.stdin.readline() @@ -173,6 +226,7 @@ class Console(): subject -- message subject message -- the message itself """ + print(is_private) if not args.callsign: args.callsign = self._read_line("Callsign:") if not args.subject: @@ -180,21 +234,10 @@ class Console(): if not args.message: args.message = self._read_multiline( "Message - end with /ex on a single line:") - # with Session(self.engine) as session: - # session.add(Message( - # sender=self.calling_station.upper(), - # recipient=args.callsign.upper(), - # subject=args.subject, - # message=args.message, - # is_private=is_private - # )) - # try: - # session.commit() - # self._write_output("Message saved!") - # except Exception as e: - # session.rollback() - # self._write_output("Error saving message." - # "Contact the sysop for assistance.") + try: + self.controller.send(args, is_private=is_private) + except Exception as e: + print(e) def send_private(self, args): self.send(args, is_private=True) @@ -218,7 +261,7 @@ class Console(): self.print_greeting() # Show initial prompt to the calling user - self._write_output(self.config.config['command_prompt']) + self._write_output(self.config.command_prompt) # Parse the BBS interactive commands for the rest of time for line in sys.stdin: @@ -226,10 +269,10 @@ class Console(): args = self.parser.parser.parse_args(line.split()) args.func(args) except Exception: - if self.config.config['debug']: + if self.config.debug: raise else: pass # Show our prompt to the calling user again - self._write_output(self.config.config['command_prompt']) + self._write_output(self.config.command_prompt) diff --git a/rsbbs/controller.py b/rsbbs/controller.py index d1ba9f1..cae3971 100644 --- a/rsbbs/controller.py +++ b/rsbbs/controller.py @@ -56,10 +56,10 @@ class Controller(): The default location is the system-specific user-level data directory. """ - db_path = self.config.config['db_path'] + db_path = self.config.db_path self.engine = create_engine( 'sqlite:///' + db_path, - echo=self.config.config['debug']) + echo=self.config.debug) # Create the database schema if none exists Base.metadata.create_all(self.engine) @@ -75,7 +75,6 @@ class Controller(): message = session.get(Message, args.number) session.delete(message) session.commit() - return {} except Exception: raise @@ -84,7 +83,7 @@ class Controller(): with Session(self.engine) as session: try: statement = delete(Message).where( - Message.recipient == self.calling_station + Message.recipient == self.config.calling_station ).returning(Message) result = session.execute(statement) count = len(result.all()) @@ -98,8 +97,9 @@ class Controller(): also known as the 'mheard' (linux) or 'jheard' (KPC, etc.) log. """ try: - result = subprocess.run(['mheard'], capture_output=True, text=True) - return {'result': result} + return subprocess.run(['mheard'], capture_output=True, text=True) + except FileNotFoundError: + raise except Exception: raise @@ -107,15 +107,19 @@ class Controller(): """List all messages.""" with Session(self.engine) as session: try: - statement = select(Message).where(or_( - (Message.is_private.is_not(False)), - (Message.recipient.__eq__( - self.config.config['args'].calling_station))) + # 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) + result = session.execute( + statement, + execution_options={"prebuffer_rows": True}) except Exception: raise - return {'result': result} + return result def list_mine(self, args): """List only messages addressed to the calling station's callsign, @@ -124,9 +128,11 @@ class Controller(): with Session(self.engine) as session: try: statement = select(Message).where( - Message.recipient == self.calling_station) - result = session.execute(statement) - return {'result': result} + Message.recipient == self.config.calling_station) + result = session.execute( + statement, + execution_options={"prebuffer_rows": True}) + return result except Exception: raise @@ -140,7 +146,7 @@ class Controller(): try: statement = select(Message).where(Message.id == args.number) result = session.execute(statement).one() - return {'result': result} + return result except Exception: raise @@ -157,7 +163,7 @@ class Controller(): with Session(self.engine) as session: try: session.add(Message( - sender=self.calling_station.upper(), + sender=self.config.calling_station.upper(), recipient=args.callsign.upper(), subject=args.subject, message=args.message, diff --git a/rsbbs/parser.py b/rsbbs/parser.py index c799e84..6df8d99 100644 --- a/rsbbs/parser.py +++ b/rsbbs/parser.py @@ -26,12 +26,11 @@ from rsbbs.commands import Commands class BBSArgumentParser(argparse.ArgumentParser): # Override the error handler to prevent exiting on error - def error(self, message): + # def error(self, message): + # print(message) + # raise - print(message) - raise Exception(message) - - def exit(self): + def exit(self, arg1, arg2): pass @@ -51,7 +50,7 @@ class Parser(BBSArgumentParser): # We will create a subparser for each individual command subparsers = self.parser.add_subparsers( - title='Commands', + title='Commands', dest='command') # Loop through the commands and add each as a subparser diff --git a/rsbbs/rsbbs.py b/rsbbs/rsbbs.py index 7fab314..c82b9cd 100755 --- a/rsbbs/rsbbs.py +++ b/rsbbs/rsbbs.py @@ -66,10 +66,6 @@ def main(): # Init the contoller controller = Controller(config) - # # Set up commands and parser - # commands = Commands(controller) - # parser = Parser(commands) - # Init the UI console console = Console(config, controller)