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 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,22 +63,27 @@ 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}")
@@ -71,3 +91,12 @@ def get_bulletins_recent(client: Client, bbs_callsign: str, limit: int = None) -
for b in response.payload:
out_list.append(BulletinWrapper(b))
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.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()

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}")
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)