diff --git a/packetserver/client/bulletins.py b/packetserver/client/bulletins.py index e378704..a64ca18 100644 --- a/packetserver/client/bulletins.py +++ b/packetserver/client/bulletins.py @@ -4,11 +4,12 @@ from typing import Union, Optional import datetime import time + class BulletinWrapper: def __init__(self, data: dict): - for i in ['author', 'id', 'subject', 'body', 'created_at', 'updated_at']: - if i not in data: - raise ValueError("Was not given a bulletin dictionary.") + for i in ['id', 'author', 'subject', 'body', 'created_at', 'updated_at']: + if i not in data.keys(): + raise ValueError("Data dict was not a bulletin dictionary.") self.data = data def __repr__(self): @@ -16,7 +17,19 @@ class BulletinWrapper: @property def id(self) -> int: - return self.data['id'] + return int(self.data['id']) + + @property + def author(self) -> str: + return str(self.data['author']).strip().upper() + + @property + def subject(self) -> str: + return str(self.data['subject']) + + @property + def body(self) -> str: + return str(self.data['body']) @property def created(self) -> datetime.datetime: @@ -26,17 +39,19 @@ class BulletinWrapper: def updated(self) -> datetime.datetime: return datetime.datetime.fromisoformat(self.data['updated_at']) - @property - def author(self) -> str: - return self.data['author'] - - @property - def subject(self) -> str: - return self.data['subject'] - - @property - def body(self) -> str: - return self.data['body'] + def to_dict(self, json=True) -> dict: + d = { + 'id': self.id, + 'author': self.author, + 'subject': self.subject, + 'body': self.body, + 'created_at': self.created, + 'updated_at': self.updated + } + if json: + d['created_at'] = d['created_at'].isoformat() + d['updated_at'] = d['updated_at'].isoformat() + return d def post_bulletin(client: Client, bbs_callsign: str, subject: str, body: str) -> int: req = Request.blank() @@ -48,26 +63,40 @@ def post_bulletin(client: Client, bbs_callsign: str, subject: str, body: str) -> raise RuntimeError(f"Posting bulletin failed: {response.status_code}: {response.payload}") return response.payload['bulletin_id'] -def get_bulletin_by_id(client: Client, bbs_callsign: str, bid: int) -> BulletinWrapper: +def get_bulletin_by_id(client: Client, bbs_callsign: str, bid: int, only_subject=False) -> BulletinWrapper: req = Request.blank() req.path = "bulletin" req.set_var('id', bid) + if only_subject: + req.set_var('no_body', True) req.method = Request.Method.GET response = client.send_receive_callsign(req, bbs_callsign) if response.status_code != 200: raise RuntimeError(f"GET bulletin {bid} failed: {response.status_code}: {response.payload}") return BulletinWrapper(response.payload) -def get_bulletins_recent(client: Client, bbs_callsign: str, limit: int = None) -> list[BulletinWrapper]: +def get_bulletins_recent(client: Client, bbs_callsign: str, limit: int = None, + only_subject=False) -> list[BulletinWrapper]: req = Request.blank() req.path = "bulletin" req.method = Request.Method.GET if limit is not None: req.set_var('limit', limit) + if only_subject: + req.set_var('no_body', True) response = client.send_receive_callsign(req, bbs_callsign) if response.status_code != 200: raise RuntimeError(f"Listing bulletins failed: {response.status_code}: {response.payload}") out_list = [] for b in response.payload: out_list.append(BulletinWrapper(b)) - return out_list \ No newline at end of file + return out_list + +def delete_bulletin_by_id(client: Client, bbs_callsign: str, bid: int): + req = Request.blank() + req.path = "bulletin" + req.set_var('id', bid) + req.method = Request.Method.DELETE + response = client.send_receive_callsign(req, bbs_callsign) + if response.status_code != 200: + raise RuntimeError(f"DELETE bulletin {bid} failed: {response.status_code}: {response.payload}") \ No newline at end of file diff --git a/packetserver/client/cli/__init__.py b/packetserver/client/cli/__init__.py index a9044b0..aca801d 100644 --- a/packetserver/client/cli/__init__.py +++ b/packetserver/client/cli/__init__.py @@ -8,6 +8,7 @@ from packetserver.client.cli.util import format_list_dicts, exit_client from packetserver.client.cli.job import job from packetserver.client.cli.object import objects from packetserver.client.cli.message import message +from packetserver.client.cli.bulletin import bulletin import ZODB import ZODB.FileStorage import ax25 @@ -191,6 +192,7 @@ cli.add_command(job, name='job') cli.add_command(objects, name='object') cli.add_command(set_user, name='set') cli.add_command(message) +cli.add_command(bulletin) if __name__ == '__main__': cli() diff --git a/packetserver/client/cli/bulletin.py b/packetserver/client/cli/bulletin.py index e69de29..b66ea04 100644 --- a/packetserver/client/cli/bulletin.py +++ b/packetserver/client/cli/bulletin.py @@ -0,0 +1,86 @@ +import click +from packetserver.client.bulletins import (get_bulletins_recent, get_bulletin_by_id, delete_bulletin_by_id, + post_bulletin, BulletinWrapper) +from packetserver.client.cli.util import exit_client, format_list_dicts +from copy import deepcopy +import datetime +import sys +import os.path + +@click.group() +@click.pass_context +def bulletin(ctx): + """List and create bulletins on the BBS.""" + pass + + +@click.command() +@click.argument("subject", type=str) +@click.argument("body", type=str) +@click.option('--from-file', '-f', is_flag=True, default=False, + help="Get body text from file or stdin ('-').") +@click.pass_context +def post(ctx, subject, body, from_file): + client = ctx.obj['client'] + bbs = ctx.obj['bbs'] + if subject.strip() == "": + exit_client(ctx.obj, 1, message="Can't have empty subject.") + + text = "" + if from_file: + if body.strip() == "-": + text = sys.stdin.read() + else: + if not os.path.isfile(body): + exit_client(ctx.obj, 2, message=f"file {body} does not exist.") + text = open(body, 'r').read() + else: + text = body + + try: + bid = post_bulletin(client, bbs, subject, text) + exit_client(ctx.obj,0, message=f"Created bulletin #{bid}!") + except Exception as e: + exit_client(ctx.obj, 4, message=str(e)) + + +@click.command() +@click.option('--number', '-n', type=int, default=0, help="Number of bulletins to retrieve; default all.") +@click.option("--only-subject", '-S', is_flag=True, default=False, help="If set, don't retrieve body text.") +@click.option("--output-format", "-f", default="table", help="Print data as table[default], list, or JSON", + type=click.Choice(['table', 'json', 'list'], case_sensitive=False)) +@click.pass_context +def list_bulletin(ctx, number, only_subject, output_format): + client = ctx.obj['client'] + bbs = ctx.obj['bbs'] + if number == 0: + number = None + + try: + bulletins = get_bulletins_recent(client, bbs, limit=number, only_subject=only_subject) + bulletin_dicts = [b.to_dict(json=True) for b in bulletins] + exit_client(ctx.obj, 0, message=format_list_dicts(bulletin_dicts, output_format=output_format)) + except Exception as e: + exit_client(ctx.obj, 2, message=str(e)) + + +@click.command() +@click.argument("bid", metavar="", type=int) +@click.option("--only-subject", '-S', is_flag=True, default=False, help="If set, don't retrieve body text.") +@click.option("--output-format", "-f", default="table", help="Print data as table[default], list, or JSON", + type=click.Choice(['table', 'json', 'list'], case_sensitive=False)) +@click.pass_context +def get(ctx, bid, only_subject, output_format): + client = ctx.obj['client'] + bbs = ctx.obj['bbs'] + + try: + bulletins = [get_bulletin_by_id(client, bbs, bid, only_subject=only_subject)] + bulletin_dicts = [b.to_dict(json=True) for b in bulletins] + exit_client(ctx.obj, 0, message=format_list_dicts(bulletin_dicts, output_format=output_format)) + except Exception as e: + exit_client(ctx.obj, 2, message=str(e)) + +bulletin.add_command(post) +bulletin.add_command(list_bulletin, name = "list") +bulletin.add_command(get) diff --git a/packetserver/server/bulletin.py b/packetserver/server/bulletin.py index 76f91f6..e48e5aa 100644 --- a/packetserver/server/bulletin.py +++ b/packetserver/server/bulletin.py @@ -82,6 +82,10 @@ def handle_bulletin_get(req: Request, conn: PacketServerConnection, db: ZODB.DB) logging.debug(f"bulletin get path: {sp}") bid = None limit = None + only_subject = False + if 'no_body' in req.vars: + if type(req.vars['no_body']) is bool: + only_subject = req.vars['no_body'] if 'limit' in req.vars: try: limit = int(req.vars['limit']) @@ -107,6 +111,8 @@ def handle_bulletin_get(req: Request, conn: PacketServerConnection, db: ZODB.DB) bull = Bulletin.get_bulletin_by_id(bid, db.root()) if bull: response.payload = bull.to_dict() + if only_subject: + response.payload['body'] = '' response.status_code = 200 else: response.status_code = 404 @@ -114,6 +120,9 @@ def handle_bulletin_get(req: Request, conn: PacketServerConnection, db: ZODB.DB) logging.debug(f"retrieving all bulletins") bulls = Bulletin.get_recent_bulletins(db.root(), limit=limit) response.payload = [bulletin.to_dict() for bulletin in bulls] + if only_subject: + for b in response.payload: + b['body'] = '' response.status_code = 200 send_response(conn, response, req) @@ -139,10 +148,38 @@ def handle_bulletin_update(req: Request, conn: PacketServerConnection, db: ZODB. send_response(conn, response, req) def handle_bulletin_delete(req: Request, conn: PacketServerConnection, db: ZODB.DB): # TODO - response = Response.blank() + username = ax25.Address(conn.remote_callsign).call.upper().strip() + sp = req.path.split("/") + if len(sp) > 1: + logging.debug(f"checking path for bulletin id") + try: + logging.debug(f"{sp[1]}") + bid = int(sp[1].strip()) + except ValueError: + send_blank_response(conn, req, 400, "Invalid path.") + return + elif 'id' in req.vars: + try: + bid = int(req.vars['id']) + except ValueError: + send_blank_response(conn, req, 400, "Invalid id.") + return + else: + send_blank_response(conn, req, 400) + return + with db.transaction() as db: - pass - send_response(conn, response, req) + bull = Bulletin.get_bulletin_by_id(bid, db.root()) + if bull: + if username != bull.author: + send_blank_response(conn, req, 401) + return + db.root.bulletins.remove(bull) + send_blank_response(conn, req, 200) + return + else: + send_blank_response(conn, req, 404) + def bulletin_root_handler(req: Request, conn: PacketServerConnection, db: ZODB.DB): logging.debug(f"{req} being processed by bulletin_root_handler") @@ -155,5 +192,7 @@ def bulletin_root_handler(req: Request, conn: PacketServerConnection, db: ZODB.D handle_bulletin_get(req, conn, db) elif req.method is Request.Method.POST: handle_bulletin_post(req, conn, db) + elif req.method is Request.Method.DELETE: + handle_bulletin_delete(req, conn ,db) else: send_blank_response(conn, req, status_code=404)