Working on messaging API and database now.

This commit is contained in:
Michael Woods
2025-01-07 20:35:27 -05:00
parent 06b27ba011
commit 5918afdf94
3 changed files with 251 additions and 5 deletions

View File

@@ -63,6 +63,9 @@ class Server:
if 'users' not in conn.root(): if 'users' not in conn.root():
logging.debug("users missing, creating bucket") logging.debug("users missing, creating bucket")
conn.root.users = PersistentMapping() conn.root.users = PersistentMapping()
if 'messages' not in conn.root():
logging.debug("messages container missing, creating bucket")
conn.root.users = PersistentMapping()
if 'SYSTEM' not in conn.root.users: if 'SYSTEM' not in conn.root.users:
logging.debug("Creating system user for first time.") logging.debug("Creating system user for first time.")
User('SYSTEM', hidden=True, enabled=False).write_new(conn.root()) User('SYSTEM', hidden=True, enabled=False).write_new(conn.root())

View File

@@ -2,4 +2,5 @@
default_server_config = { default_server_config = {
"motd": "Welcome to this PacketServer BBS!", "motd": "Welcome to this PacketServer BBS!",
"operator": "placeholder", "operator": "placeholder",
"max_message_length": 2000
} }

View File

@@ -4,17 +4,259 @@ import persistent
import persistent.list import persistent.list
from persistent.mapping import PersistentMapping from persistent.mapping import PersistentMapping
import datetime import datetime
from typing import Self,Union,Optional from typing import Self,Union,Optional,Iterable,Sequence
from packetserver.common import PacketServerConnection, Request, Response, Message, send_response, send_blank_response from packetserver.common import PacketServerConnection, Request, Response, send_response, send_blank_response
from packetserver.common import Message as PacketMessage
import ZODB import ZODB
import logging import logging
import uuid import uuid
from uuid import UUID from uuid import UUID
from packetserver.common.util import email_valid from packetserver.common.util import email_valid
from packetserver.server.objects import Object
from BTrees.OOBTree import TreeSet
from packetserver.server.users import User, user_authorized
from traceback import format_exc
from collections import namedtuple
# TODO all messages
def mailbox_create(username: str, db_root: PersistentMapping):
un = username.upper().strip()
if un not in db_root['messages']:
db_root['messages'][un] = persistent.list.PersistentList()
def global_unique_message_uuid(db_root: PersistentMapping) -> UUID:
if "message_uuids" not in db_root:
db_root['message_uuids'] = TreeSet()
logging.debug("Created message_uuid set for global message ids.")
uid = uuid.uuid4()
while uid in db_root['message_uuids']:
uid = uuid.uuid4()
return uid
class Attachment: class Attachment:
"""Name and data that is sent with a message.""" """Name and data that is sent with a message."""
def __init__(self, name: str, ): def __init__(self, name: str, data: Union[bytes,bytearray,str]):
pass self._name = ""
self._data = b""
self._binary = True
self.data = data
self.name = name
@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()
@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
else:
if str(data).encode() != self._data:
self._data = str(data).encode()
self._binary = False
@property
def size(self) -> int:
return len(self.data)
def copy(self):
return Attachment(self.name, self.data)
class ObjectAttachment(Attachment):
def __init__(self, name: str, obj: Object):
self.object = obj
super().__init__(name, "")
@property
def size(self) -> int:
return self.object.size
@property
def data(self) -> Union[str,bytes]:
return self.object.data
@property
def binary(self) -> bool:
return self.object.binary
class MessageTextTooLongError(Exception):
"""Raised when the message text exceeds the length allowed in the server config."""
pass
class MessageAlreadySentError(Exception):
"""Raised when the message text exceeds the length allowed in the server config."""
pass
class Message(persistent.Persistent):
def __init__(self, text: str, msg_to: Optional[Iterable[str],str]= None, msg_from: Optional[str] = None,
attachments: Optional[Iterable[Attachment]] = None):
self.retrieved = False
self.sent_at = datetime.datetime.now(datetime.UTC)
self.text = text
self.attachments = ()
self.msg_to = (None,)
self.msg_from = None
self.msg_id = uuid.uuid4()
self.msg_delivered = False
if msg_to:
if type(msg_to) is str:
msg_to = msg_to.upper().strip()
self.msg_to = (msg_to,)
else:
msg_to_tmp = []
for i in msg_to:
i = str(i).strip().upper()
if i == "ALL":
msg_to_tmp = ["ALL"]
break
else:
msg_to_tmp.append(i)
self.msg_to = tuple(msg_to_tmp)
if msg_from:
self.msg_from = str(msg_from).upper().strip()
if attachments:
attch = []
for i in attachments:
if not isinstance(i,Attachment):
attch.append(Attachment("",str(i)))
else:
attch.append(i)
self.attachments = tuple(attch)
def __repr__(self):
return f"<Message: ID: {self.msg_id}, Sent: {self.msg_delivered}>"
def send(self, db: ZODB.DB) -> tuple:
if self.msg_delivered:
raise MessageAlreadySentError("Cannot send a private message that has already been sent.")
if self.msg_from is None:
raise ValueError("Message sender (message_from) cannot be None.")
new_attachments = []
for i in self.attachments:
if isinstance(i,ObjectAttachment):
new_attachments.append(Attachment(i.name, i.data))
else:
new_attachments.append(i)
send_counter = 0
recipients = []
failed = []
to_all = False
with db.transaction() as db:
for recipient in self.msg_to:
recipient = recipient.upper().strip()
if recipient is None:
continue
if recipient == "ALL":
recipients = [x for x in db.root.users if db.root.users[x].enabled]
to_all = True
break
recipients.append(recipient)
for recipient in recipients:
msg = Message(self.text, recipient, self.msg_from, attachments=[x.copy() for x in new_attachments])
try:
mailbox_create(recipient, db.root())
msg.msg_id = global_unique_message_uuid(db.root())
msg.msg_delivered = True
msg.sent_at = datetime.datetime.now(datetime.UTC)
if to_all:
msg.msg_to = 'ALL'
db.root.messages[recipient].append(msg)
send_counter = send_counter + 1
except:
logging.error(f"Error sending message to {recipient}:\n{format_exc()}")
failed.append(recipient)
return send_counter, failed
DisplayOptions = namedtuple('DisplayOptions', ['get_text', 'limit', 'sort_by', 'reverse', 'search',
'get_attachments', 'sent_unsent'])
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 handle_message_get(req: Request, conn: PacketServerConnection, db: ZODB.DB):
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_message_get(req, conn, db)
else:
send_blank_response(conn, req, status_code=404)