Bulletins can be created and retrieved by the CLI now.

This commit is contained in:
Michael Woods
2025-03-19 23:32:58 -04:00
parent 53cdfaf312
commit be9871c832
4 changed files with 177 additions and 21 deletions

View File

@@ -4,11 +4,12 @@ from typing import Union, Optional
import datetime import datetime
import time import time
class BulletinWrapper: class BulletinWrapper:
def __init__(self, data: dict): def __init__(self, data: dict):
for i in ['author', 'id', 'subject', 'body', 'created_at', 'updated_at']: for i in ['id', 'author', 'subject', 'body', 'created_at', 'updated_at']:
if i not in data: if i not in data.keys():
raise ValueError("Was not given a bulletin dictionary.") raise ValueError("Data dict was not a bulletin dictionary.")
self.data = data self.data = data
def __repr__(self): def __repr__(self):
@@ -16,7 +17,19 @@ class BulletinWrapper:
@property @property
def id(self) -> int: 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 @property
def created(self) -> datetime.datetime: def created(self) -> datetime.datetime:
@@ -26,17 +39,19 @@ class BulletinWrapper:
def updated(self) -> datetime.datetime: def updated(self) -> datetime.datetime:
return datetime.datetime.fromisoformat(self.data['updated_at']) return datetime.datetime.fromisoformat(self.data['updated_at'])
@property def to_dict(self, json=True) -> dict:
def author(self) -> str: d = {
return self.data['author'] 'id': self.id,
'author': self.author,
@property 'subject': self.subject,
def subject(self) -> str: 'body': self.body,
return self.data['subject'] 'created_at': self.created,
'updated_at': self.updated
@property }
def body(self) -> str: if json:
return self.data['body'] 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: def post_bulletin(client: Client, bbs_callsign: str, subject: str, body: str) -> int:
req = Request.blank() 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}") raise RuntimeError(f"Posting bulletin failed: {response.status_code}: {response.payload}")
return response.payload['bulletin_id'] 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 = Request.blank()
req.path = "bulletin" req.path = "bulletin"
req.set_var('id', bid) req.set_var('id', bid)
if only_subject:
req.set_var('no_body', True)
req.method = Request.Method.GET req.method = Request.Method.GET
response = client.send_receive_callsign(req, bbs_callsign) response = client.send_receive_callsign(req, bbs_callsign)
if response.status_code != 200: if response.status_code != 200:
raise RuntimeError(f"GET bulletin {bid} failed: {response.status_code}: {response.payload}") raise RuntimeError(f"GET bulletin {bid} failed: {response.status_code}: {response.payload}")
return BulletinWrapper(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 = Request.blank()
req.path = "bulletin" req.path = "bulletin"
req.method = Request.Method.GET req.method = Request.Method.GET
if limit is not None: if limit is not None:
req.set_var('limit', limit) req.set_var('limit', limit)
if only_subject:
req.set_var('no_body', True)
response = client.send_receive_callsign(req, bbs_callsign) response = client.send_receive_callsign(req, bbs_callsign)
if response.status_code != 200: if response.status_code != 200:
raise RuntimeError(f"Listing bulletins failed: {response.status_code}: {response.payload}") raise RuntimeError(f"Listing bulletins failed: {response.status_code}: {response.payload}")
out_list = [] out_list = []
for b in response.payload: for b in response.payload:
out_list.append(BulletinWrapper(b)) out_list.append(BulletinWrapper(b))
return out_list 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}")

View File

@@ -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.job import job
from packetserver.client.cli.object import objects from packetserver.client.cli.object import objects
from packetserver.client.cli.message import message from packetserver.client.cli.message import message
from packetserver.client.cli.bulletin import bulletin
import ZODB import ZODB
import ZODB.FileStorage import ZODB.FileStorage
import ax25 import ax25
@@ -191,6 +192,7 @@ cli.add_command(job, name='job')
cli.add_command(objects, name='object') cli.add_command(objects, name='object')
cli.add_command(set_user, name='set') cli.add_command(set_user, name='set')
cli.add_command(message) cli.add_command(message)
cli.add_command(bulletin)
if __name__ == '__main__': if __name__ == '__main__':
cli() cli()

View File

@@ -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="<BULLETIN ID>", 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)

View File

@@ -82,6 +82,10 @@ def handle_bulletin_get(req: Request, conn: PacketServerConnection, db: ZODB.DB)
logging.debug(f"bulletin get path: {sp}") logging.debug(f"bulletin get path: {sp}")
bid = None bid = None
limit = 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: if 'limit' in req.vars:
try: try:
limit = int(req.vars['limit']) 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()) bull = Bulletin.get_bulletin_by_id(bid, db.root())
if bull: if bull:
response.payload = bull.to_dict() response.payload = bull.to_dict()
if only_subject:
response.payload['body'] = ''
response.status_code = 200 response.status_code = 200
else: else:
response.status_code = 404 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") logging.debug(f"retrieving all bulletins")
bulls = Bulletin.get_recent_bulletins(db.root(), limit=limit) bulls = Bulletin.get_recent_bulletins(db.root(), limit=limit)
response.payload = [bulletin.to_dict() for bulletin in bulls] response.payload = [bulletin.to_dict() for bulletin in bulls]
if only_subject:
for b in response.payload:
b['body'] = ''
response.status_code = 200 response.status_code = 200
send_response(conn, response, req) send_response(conn, response, req)
@@ -139,10 +148,38 @@ def handle_bulletin_update(req: Request, conn: PacketServerConnection, db: ZODB.
send_response(conn, response, req) send_response(conn, response, req)
def handle_bulletin_delete(req: Request, conn: PacketServerConnection, db: ZODB.DB): # TODO 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: with db.transaction() as db:
pass bull = Bulletin.get_bulletin_by_id(bid, db.root())
send_response(conn, response, req) 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): def bulletin_root_handler(req: Request, conn: PacketServerConnection, db: ZODB.DB):
logging.debug(f"{req} being processed by bulletin_root_handler") 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) handle_bulletin_get(req, conn, db)
elif req.method is Request.Method.POST: elif req.method is Request.Method.POST:
handle_bulletin_post(req, conn, db) handle_bulletin_post(req, conn, db)
elif req.method is Request.Method.DELETE:
handle_bulletin_delete(req, conn ,db)
else: else:
send_blank_response(conn, req, status_code=404) send_blank_response(conn, req, status_code=404)