Adding object support. Not completed or tested at all.

This commit is contained in:
Michael Woods
2025-01-05 18:59:31 -05:00
parent f57cdbadd8
commit c2ed95fe6a
5 changed files with 367 additions and 2 deletions

View File

@@ -16,7 +16,7 @@ import time
from msgpack.exceptions import OutOfData
from typing import Callable, Self, Union
from traceback import format_exc
from os import linesep
def init_bulletins(root: PersistentMapping):
if 'bulletins' not in root:
@@ -66,6 +66,9 @@ class Server:
if 'SYSTEM' not in conn.root.users:
logging.debug("Creating system user for first time.")
User('SYSTEM', hidden=True, enabled=False).write_new(conn.root())
if 'objects' not in conn.root():
logging.debug("objects bucket missing, creating")
conn.root.objects = OOBTree()
init_bulletins(conn.root())
self.app = pe.app.Application()
PacketServerConnection.receive_subscribers.append(lambda x: self.server_receiver(x))
@@ -170,6 +173,12 @@ class Server:
self.zeo_stop = stop
self.db = ZEO.DB(self.zeo_addr)
logging.info(f"Starting ZEO server with address {self.zeo_addr}")
try:
zeo_address_file = str(self.home_dir.joinpath("zeo-address.txt"))
open(zeo_address_file, 'w').write(f"{self.zeo_addr[0]}:{self.zeo_addr[1]}{linesep}")
logging.info(f"Wrote ZEO server info to '{zeo_address_file}'")
except:
logging.warning(f"Couldn't write ZEO server info to '{zeo_address_file}'\n{format_exc()}")
self.app.start(self.pe_server, self.pe_port)
self.app.register_callsigns(self.callsign)

View File

@@ -0,0 +1,20 @@
"""BBS private message system"""
import ax25
import persistent
import persistent.list
from persistent.mapping import PersistentMapping
import datetime
from typing import Self,Union,Optional
from packetserver.common import PacketServerConnection, Request, Response, Message, send_response, send_blank_response
import ZODB
import logging
import uuid
from uuid import UUID
from packetserver.common.util import email_valid
# TODO all messages
class Attachment:
"""Name and data that is sent with a message."""
def __init__(self, name: str, ):
pass

View File

@@ -0,0 +1,316 @@
"""Server object storage system."""
from copy import deepcopy
import persistent
import ax25
import persistent.list
from persistent.mapping import PersistentMapping
import datetime
from typing import Self,Union,Optional
from packetserver.common import PacketServerConnection, Request, Response, Message, send_response, send_blank_response
import ZODB
import logging
import uuid
from uuid import UUID
from packetserver.server.users import User, user_authorized
from collections import namedtuple
class Object(persistent.Persistent):
def __init__(self, name: str = "", data: Union[bytes,bytearray,str] = None):
self.private = False
self._binary = False
self._data = b''
self._name = ""
self._owner = None
if data:
self.data = data
if name:
self._name = name
self._uuid = None
self.created_at = datetime.datetime.now(datetime.UTC)
self.modified_at = datetime.datetime.now(datetime.UTC)
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str):
if name.strip() != self._name:
if len(name.strip()) > 300:
raise ValueError("Object names must be no more than 300 characters.")
self._name = name.strip()
self.touch()
def touch(self):
self.modified_at = datetime.datetime.now(datetime.UTC)
@property
def size(self) -> int:
return len(self.data)
@property
def binary(self):
return self._binary
@property
def data(self) -> Union[str,bytes]:
if self.binary:
return self._data
else:
return self._data.decode()
@data.setter
def data(self, data: Union[bytes,bytearray,str]):
if type(data) in (bytes,bytearray):
if bytes(data) != self._data:
self._data = bytes(data)
self._binary = True
self.touch()
else:
if str(data).encode() != self._data:
self._data = str(data).encode()
self._binary = False
self.touch()
@property
def owner(self) -> Optional[UUID]:
return self._owner
@owner.setter
def owner(self, owner_uuid: UUID):
if owner_uuid:
if type(owner_uuid) is UUID:
self._owner = owner_uuid
self.touch()
else:
raise ValueError("Owner must be a UUID")
else:
self._owner = None
self.touch()
def chown(self, username: str, db: ZODB.DB):
un = username.strip().upper()
old_owner_uuid = self._owner
with db.transaction() as db:
user = User.get_user_by_username(username, db.root())
old_owner = User.get_user_by_uuid(old_owner_uuid, db.root())
if user:
db.root.objects[self.uuid].owner = user.uuid
user.add_obj_uuid(self.uuid)
if old_owner_uuid:
if old_owner:
old_owner.remove_obj_uuid(self.uuid)
else:
raise KeyError(f"User '{un}' not found.")
@classmethod
def get_object_by_uuid(cls, obj: UUID, db_root: PersistentMapping):
return db_root['objects'].get(obj)
@classmethod
def get_objects_by_username(cls, username: str, db: ZODB.DB) -> list[Self]:
un = username.strip().upper()
objs = []
with db.transaction() as db:
user = User.get_user_by_username(username, db.root())
if user:
uuids = user.object_uuids
for u in uuids:
try:
obj = cls.get_object_by_uuid(u, db)
if obj:
objs.append(obj)
except:
pass
return objs
@property
def uuid(self) -> Optional[UUID]:
return self._uuid
def write_new(self, db: ZODB.DB) -> UUID:
if self.uuid:
raise KeyError("Object already has UUID. Manually clear it to write it again.")
self._uuid = uuid.uuid4()
with db.transaction() as db:
while self.uuid in db.root.objects:
self._uuid = uuid.uuid4()
db.root.objects[self.uuid] = self
self.touch()
return self.uuid
def to_dict(self, include_data: bool = True) -> dict:
data = b''
if include_data:
data = self.data
return {
"name": self.name,
"uuid_bytes": self.uuid.bytes,
"size_bytes": self.size,
"binary": self.binary,
"private": self.private,
"created_at": self.created_at,
"modified_at": self.modified_at,
"includes_data": include_data,
"data": data
}
@classmethod
def from_dict(cls, obj: dict) -> Self:
o = Object(name=obj['name'])
o._uuid = UUID(bytes=obj['uuid_bytes'])
o.private = obj['private']
o.data = obj['data']
o._binary = obj['binary']
return o
def authorized_write(self, username: str, db: ZODB.DB):
un = username.strip().upper()
with db.transaction() as db:
user = User.get_user_by_username(username, db.root())
if user:
if user.uuid == self.owner:
return True
else:
return False
def authorized_get(self, username: str, db: ZODB.DB):
if not self.private:
return True
un = username.strip().upper()
with db.transaction() as db:
user = User.get_user_by_username(username, db.root())
if user:
if user.uuid == self.owner:
return True
else:
return False
DisplayOptions = namedtuple('DisplayOptions', ['get_data', 'limit', 'sort_by', 'reverse', 'search'])
def parse_display_options(req: Request) -> DisplayOptions:
limit = req.vars.get('limit')
try:
limit = int(limit)
except:
limit = None
d = req.vars.get('fetch')
if type(d) is str:
d.lower().strip()
if d in [1, 'y', True, 'yes', 'true', 't']:
get_data = True
else:
get_data = False
r = req.vars.get('reverse')
if type(r) is str:
r.lower().strip()
if r in [1, 'y', True, 'yes', 'true', 't']:
reverse = True
else:
reverse = False
sort = req.vars.get('sort')
sort_by = "name"
if type(sort) is str:
sort = sort.lower().strip()
if sort == "date":
sort_by = "date"
elif sort == "size":
sort_by = "size"
s = req.vars.get('search')
search = None
if type(s) is str:
s = s.lower()
if s:
search = str(s)
return DisplayOptions(get_data, limit, sort_by, reverse, search)
def object_display_filter(source: list[Object], opts: DisplayOptions) -> list[dict]:
if opts.search:
objs = [x for x in source if str(opts.search) in x.name.lower()]
else:
objs = deepcopy(source)
if opts.sort_by == "size":
objs.sort(key=lambda x: x.size, reverse=opts.reverse)
elif opts.sort_by == "date":
objs.sort(key=lambda x: x.modified_at, reverse=opts.reverse)
else:
objs.sort(key=lambda x: x.name, reverse=opts.reverse)
if opts.limit:
if len(objs) >= opts.limit:
objs = objs[:opts.limit]
return [o.to_dict(include_data=opts.get_data) for o in objs]
def handle_get_no_path(req: Request, conn: PacketServerConnection, db: ZODB.DB):
opts = parse_display_options(req)
response = Response.blank()
response.status_code = 404
username = ax25.Address(conn.remote_callsign).call.upper().strip()
with db.transaction() as db:
user = User.get_user_by_username(username, db.root())
if not user:
send_blank_response(conn, req, status_code=500, payload="Unknown user account problem")
return
if 'uuid' in req.vars:
uid = req.vars['uuid']
if type(uid) is bytes:
obj = Object.get_object_by_uuid(UUID(bytes=uid), db.root())
if obj:
if not obj.owner == user.uuid:
if not obj.private:
send_blank_response(conn, req, status_code=401)
return
if opts.get_data:
response.payload = obj.to_dict()
response.status_code = 200
else:
response.payload = obj.to_dict(include_data=False)
response.status_code = 200
else:
uuids = User.object_uuids
objs = []
for i in uuids:
obj = Object.get_object_by_uuid(i, db.root())
if not obj.private:
objs.append(obj)
else:
if obj.uuid == user.uuid:
objs.append(obj)
response.payload = object_display_filter(objs, opts)
response.status_code = 200
send_response(conn, response, req)
def handle_object_get(req: Request, conn: PacketServerConnection, db: ZODB.DB):
# Case: User searching their own objects -> list
# or passes specific UUID as var -> Object
handle_get_no_path(req, conn, db)
def handle_object_post(req: Request, conn: PacketServerConnection, db: ZODB.DB):
# TODO
pass
def object_root_handler(req: Request, conn: PacketServerConnection, db: ZODB.DB):
logging.debug(f"{req} being processed by user_root_handler")
if not user_authorized(conn, db):
logging.debug(f"user {conn.remote_callsign} not authorized")
send_blank_response(conn, req, status_code=401)
return
logging.debug("user is authorized")
if req.method is Request.Method.GET:
handle_object_get(req, conn, db)
elif req.method is Request.Method.POST:
handle_object_post(req, conn, db)
else:
send_blank_response(conn, req, status_code=404)

View File

@@ -4,6 +4,7 @@ from msgpack.exceptions import OutOfData
from packetserver.common import Message, Request, Response, PacketServerConnection, send_response, send_blank_response
from .bulletin import bulletin_root_handler
from .users import user_root_handler, user_authorized
from .objects import object_root_handler
import logging
from typing import Union
import ZODB
@@ -46,7 +47,8 @@ def root_root_handler(req: Request, conn: PacketServerConnection,
standard_handlers = {
"": root_root_handler,
"bulletin": bulletin_root_handler,
"user": user_root_handler
"user": user_root_handler,
"object": object_root_handler
}

View File

@@ -3,6 +3,7 @@
import ax25
import persistent
import persistent.list
from persistent.list import PersistentList
from persistent.mapping import PersistentMapping
import datetime
from typing import Self,Union,Optional
@@ -12,6 +13,7 @@ import logging
import uuid
from uuid import UUID
from packetserver.common.util import email_valid
from BTrees.OOBTree import TreeSet
class User(persistent.Persistent):
def __init__(self, username: str, enabled: bool = True, hidden: bool = False, bio: str = "", status: str = "",
@@ -33,6 +35,7 @@ class User(persistent.Persistent):
self.bio = bio
self._status = ""
self.status = status
self._objects = TreeSet()
def write_new(self, db_root: PersistentMapping):
all_uuids = [db_root['users'][x].uuid for x in db_root['users']]
@@ -43,6 +46,21 @@ class User(persistent.Persistent):
if self.username not in db_root['users']:
db_root['users'][self.username] = self
@property
def object_uuids(self) -> list[UUID]:
return list(self._objects)
def remove_obj_uuid(self, obj: UUID):
self._objects.remove(obj)
def add_obj_uuid(self, obj: UUID):
self._objects.add(obj)
def user_has_obj(self, obj: UUID) -> bool:
if obj in self._objects:
return True
return False
@property
def location(self) -> str:
return self._location