Working on messaging API and database now.
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
default_server_config = {
|
||||
"motd": "Welcome to this PacketServer BBS!",
|
||||
"operator": "placeholder",
|
||||
"max_message_length": 2000
|
||||
}
|
||||
@@ -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, ):
|
||||
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"<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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user