Working on messaging API and database now.
This commit is contained in:
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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]):
|
||||||
|
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
|
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