diff --git a/src/packetserver/server/__init__.py b/src/packetserver/server/__init__.py index 7b56857..ec3c562 100644 --- a/src/packetserver/server/__init__.py +++ b/src/packetserver/server/__init__.py @@ -63,6 +63,9 @@ class Server: if 'users' not in conn.root(): logging.debug("users missing, creating bucket") 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: logging.debug("Creating system user for first time.") User('SYSTEM', hidden=True, enabled=False).write_new(conn.root()) diff --git a/src/packetserver/server/constants.py b/src/packetserver/server/constants.py index 48fee2b..a80ca1e 100644 --- a/src/packetserver/server/constants.py +++ b/src/packetserver/server/constants.py @@ -2,4 +2,5 @@ default_server_config = { "motd": "Welcome to this PacketServer BBS!", "operator": "placeholder", + "max_message_length": 2000 } \ No newline at end of file diff --git a/src/packetserver/server/messages.py b/src/packetserver/server/messages.py index 0635119..55f5e97 100644 --- a/src/packetserver/server/messages.py +++ b/src/packetserver/server/messages.py @@ -4,17 +4,259 @@ 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 +from typing import Self,Union,Optional,Iterable,Sequence +from packetserver.common import PacketServerConnection, Request, Response, send_response, send_blank_response +from packetserver.common import Message as PacketMessage import ZODB import logging import uuid from uuid import UUID 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: """Name and data that is sent with a message.""" - def __init__(self, name: str, ): - pass \ No newline at end of file + def __init__(self, name: str, data: Union[bytes,bytearray,str]): + 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"" + + 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) + + + + + + + + + + + +