Messages client done. Messages server improved slightly.

This commit is contained in:
Michael Woods
2025-02-17 13:52:44 -05:00
parent 3ba5589e91
commit 7832792aa6
2 changed files with 233 additions and 28 deletions

View File

@@ -2,33 +2,190 @@ import datetime
from packetserver.client import Client from packetserver.client import Client
from packetserver.common import Request, Response, PacketServerConnection from packetserver.common import Request, Response, PacketServerConnection
from packetserver.common.util import to_date_digits
from typing import Union, Optional from typing import Union, Optional
from uuid import UUID, uuid4 from uuid import UUID, uuid4
import os.path import os.path
# TODO messages client
class AttachmentWrapper:
def __init__(self, data: dict):
for i in ['name', 'binary', 'data']:
if i not in data.keys():
raise ValueError("Data dict was not an attachment dictionary.")
self._data = data
def __repr__(self):
return f"<AttachmentWrapper: {self.name}>"
@property
def name(self) -> str:
return self._data['name']
@property
def binary(self) -> bool:
return self._data['binary']
@property
def data(self) -> Union[str,bytes]:
if self.binary:
return self._data['data']
else:
return self._data['data'].decode()
class MessageWrapper: class MessageWrapper:
# TODO MessageWrapper
def __init__(self, data: dict): def __init__(self, data: dict):
for i in ['username', 'status', 'bio', 'socials', 'created_at', 'last_seen', 'email', 'location']: for i in ['attachments', 'to', 'from', 'id', 'sent_at', 'text']:
if i not in data.keys(): if i not in data.keys():
raise ValueError("Data dict was not an object dictionary.") raise ValueError("Data dict was not a message dictionary.")
self.data = data self.data = data
@property
def text(self) -> str:
return self.data['text']
def send_message(client: Client, bbs_callsign: str,): @property
# TODO send message def sent(self) -> datetime.datetime:
pass return datetime.datetime.fromisoformat(self.data['sent_at'])
def get_message_uuid(): @property
# TODO get message by uuid def msg_id(self) -> UUID:
pass return UUID(self.data['id'])
def get_messages_since(): @property
# TODO get messages since date def from_user(self) -> str:
pass return self.data['from']
def get_messages(): @property
# TODO get messages default def to_users(self) -> list[str]:
pass return self.data['to']
@property
def attachments(self) -> list[AttachmentWrapper]:
a_list = []
for a in self.data['attachments']:
a_list.append(AttachmentWrapper(a))
return a_list
class MsgAttachment:
def __init__(self, name: str, data: Union[bytes,str]):
self.binary = True
self.name = name
if type(data) in [bytes, bytearray]:
self.data = data
else:
self.data = str(data).encode()
self.binary = False
def __repr__(self) -> str:
return f"<MsgAttachment {self.name}>"
def to_dict(self) -> dict:
return {
"name": self.name,
"data": self.data,
"binary": self.binary
}
def attachment_from_file(filename: str, binary: bool = True) -> MsgAttachment:
a = MsgAttachment(os.path.basename(filename), open(filename, 'rb').read())
if not binary:
a.binary = False
return a
def send_message(client: Client, bbs_callsign: str, text: str, to: list[str],
attachments: list[MsgAttachment] = None) -> dict:
payload = {
"text": text,
"to": to,
"attachments": []
}
for a in attachments:
payload["attachments"].append(a.to_dict())
req = Request.blank()
req.path = "message"
req.method = Request.Method.POST
req.payload = payload
response = client.send_receive_callsign(req, bbs_callsign)
if response.status_code != 201:
raise RuntimeError(f"POST message failed: {response.status_code}: {response.payload}")
return response.payload
def get_message_uuid(client: Client, bbs_callsign: str, msg_id: UUID, ) -> MessageWrapper:
req = Request.blank()
req.path = "message"
req.method = Request.Method.GET
req.set_var('id', msg_id.bytes)
response = client.send_receive_callsign(req, bbs_callsign)
if response.status_code != 200:
raise RuntimeError(f"GET message failed: {response.status_code}: {response.payload}")
return MessageWrapper(response.payload)
def get_messages_since(client: Client, bbs_callsign: str, since: datetime.datetime, get_text: bool = True, limit: int = None,
sort_by: str = 'date', reverse: bool = False, search: str = None, get_attachments: bool = True,
source: str = 'received') -> list[MessageWrapper]:
req = Request.blank()
req.path = "message"
req.method = Request.Method.GET
# put vars together
req.set_var('since', to_date_digits(since))
source = source.lower().strip()
if source not in ['sent', 'received', 'all']:
raise ValueError("Source variable must be ['sent', 'received', 'all']")
req.set_var('source', source)
req.set_var('limit', limit)
req.set_var('fetch_text', get_text)
req.set_var('reverse', reverse)
if sort_by.strip().lower() not in ['date', 'from', 'to']:
raise ValueError("sort_by must be in ['date', 'from', 'to']")
req.set_var('sort', sort_by)
if type(search) is str:
req.set_var('search', search)
response = client.send_receive_callsign(req, bbs_callsign)
if response.status_code != 200:
raise RuntimeError(f"GET message failed: {response.status_code}: {response.payload}")
msg_list = []
for m in response.payload:
msg_list.append(MessageWrapper(m))
return msg_list
def get_messages(client: Client, bbs_callsign: str, get_text: bool = True, limit: int = None,
sort_by: str = 'date', reverse: bool = True, search: str = None, get_attachments: bool = True,
source: str = 'received') -> list[MessageWrapper]:
req = Request.blank()
req.path = "message"
req.method = Request.Method.GET
# put vars together
source = source.lower().strip()
if source not in ['sent', 'received', 'all']:
raise ValueError("Source variable must be ['sent', 'received', 'all']")
req.set_var('source', source)
req.set_var('limit', limit)
req.set_var('fetch_text', get_text)
req.set_var('reverse', reverse)
if sort_by.strip().lower() not in ['date', 'from', 'to']:
raise ValueError("sort_by must be in ['date', 'from', 'to']")
req.set_var('sort', sort_by)
if type(search) is str:
req.set_var('search', search)
response = client.send_receive_callsign(req, bbs_callsign)
if response.status_code != 200:
raise RuntimeError(f"GET message failed: {response.status_code}: {response.payload}")
msg_list = []
for m in response.payload:
msg_list.append(MessageWrapper(m))
return msg_list

View File

@@ -15,6 +15,7 @@ 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 packetserver.server.objects import Object
from packetserver.server.users import User
from BTrees.OOBTree import TreeSet from BTrees.OOBTree import TreeSet
from packetserver.server.users import User, user_authorized from packetserver.server.users import User, user_authorized
from traceback import format_exc from traceback import format_exc
@@ -25,9 +26,15 @@ since_regex = """^message\\/since\\/(\\d+)$"""
def mailbox_create(username: str, db_root: PersistentMapping): def mailbox_create(username: str, db_root: PersistentMapping):
un = username.upper().strip() un = username.upper().strip()
u = User.get_user_by_username(un, db_root)
if u is None:
raise KeyError(f"Username {username} does not exist.")
if not u.enabled:
raise KeyError(f"Username {username} does not exist.")
if un not in db_root['messages']: if un not in db_root['messages']:
db_root['messages'][un] = persistent.list.PersistentList() db_root['messages'][un] = persistent.list.PersistentList()
def global_unique_message_uuid(db_root: PersistentMapping) -> UUID: def global_unique_message_uuid(db_root: PersistentMapping) -> UUID:
if "message_uuids" not in db_root: if "message_uuids" not in db_root:
db_root['message_uuids'] = TreeSet() db_root['message_uuids'] = TreeSet()
@@ -97,6 +104,7 @@ class Attachment:
"name": self.name, "name": self.name,
"binary": self.binary, "binary": self.binary,
"size_bytes": self.size, "size_bytes": self.size,
"data": b''
} }
if include_data: if include_data:
d['data'] = self.data d['data'] = self.data
@@ -200,7 +208,9 @@ class Message(persistent.Persistent):
new_attachments = [] new_attachments = []
for i in self.attachments: for i in self.attachments:
if isinstance(i,ObjectAttachment): if isinstance(i,ObjectAttachment):
new_attachments.append(Attachment(i.name, i.data)) logging.debug("Skpping object attachments for now. Resolve db queries for them at send time.")
# new_attachments.append(Attachment(i.name, i.data)) TODO send object attachments
pass
else: else:
new_attachments.append(i) new_attachments.append(i)
send_counter = 0 send_counter = 0
@@ -208,6 +218,8 @@ class Message(persistent.Persistent):
failed = [] failed = []
to_all = False to_all = False
with db.transaction() as db: with db.transaction() as db:
mailbox_create(self.msg_from, db.root())
self.msg_id = global_unique_message_uuid(db.root())
for recipient in self.msg_to: for recipient in self.msg_to:
recipient = recipient.upper().strip() recipient = recipient.upper().strip()
if recipient is None: if recipient is None:
@@ -217,11 +229,13 @@ class Message(persistent.Persistent):
to_all = True to_all = True
break break
recipients.append(recipient) recipients.append(recipient)
if self.msg_from.upper().strip() in recipients:
recipients.remove(self.msg_from.upper().strip())
send_counter = send_counter + 1
for recipient in recipients: for recipient in recipients:
msg = Message(self.text, recipient, self.msg_from, attachments=[x.copy() for x in new_attachments]) msg = Message(self.text, recipient, self.msg_from, attachments=[x.copy() for x in new_attachments])
try: try:
mailbox_create(recipient, db.root()) mailbox_create(recipient, db.root())
msg.msg_id = global_unique_message_uuid(db.root())
msg.msg_delivered = True msg.msg_delivered = True
msg.sent_at = datetime.datetime.now(datetime.UTC) msg.sent_at = datetime.datetime.now(datetime.UTC)
if to_all: if to_all:
@@ -231,8 +245,10 @@ class Message(persistent.Persistent):
except: except:
logging.error(f"Error sending message to {recipient}:\n{format_exc()}") logging.error(f"Error sending message to {recipient}:\n{format_exc()}")
failed.append(recipient) failed.append(recipient)
self.msg_delivered = True
return send_counter, failed self.attachments = [x.copy() for x in new_attachments]
db.root.messages[self.msg_from.upper().strip()].append(msg)
return send_counter, failed, self.msg_id
DisplayOptions = namedtuple('DisplayOptions', ['get_text', 'limit', 'sort_by', 'reverse', 'search', DisplayOptions = namedtuple('DisplayOptions', ['get_text', 'limit', 'sort_by', 'reverse', 'search',
'get_attachments', 'sent_received_all']) 'get_attachments', 'sent_received_all'])
@@ -305,7 +321,7 @@ def handle_messages_since(req: Request, conn: PacketServerConnection, db: ZODB.D
logging.warning(f"Received req with wrong message for path {req.path}.") logging.warning(f"Received req with wrong message for path {req.path}.")
return return
try: try:
since_date = from_date_digits(req.path.split("/")[2]) since_date = from_date_digits(req.vars['since'])
except ValueError as v: except ValueError as v:
send_blank_response(conn, req, 400, "invalid date string") send_blank_response(conn, req, 400, "invalid date string")
return return
@@ -347,14 +363,43 @@ def handle_messages_since(req: Request, conn: PacketServerConnection, db: ZODB.D
send_response(conn, response, req) send_response(conn, response, req)
def handle_message_get_id(req: Request, conn: PacketServerConnection, db: ZODB.DB): def handle_message_get_id(req: Request, conn: PacketServerConnection, db: ZODB.DB):
# TODO message get specific by uuid uuid_val = req.vars['id']
obj_uuid = None
try:
if type(uuid_val) is bytes:
obj_uuid = UUID(bytes=uuid_val)
elif type(uuid_val) is int:
obj_uuid = UUID(int=uuid_val)
elif type(uuid_val) is str:
obj_uuid = UUID(uuid_val)
except:
pass pass
if obj_uuid is None:
send_blank_response(conn, req, 400)
return
opts = parse_display_options(req)
username = ax25.Address(conn.remote_callsign).call.upper().strip()
msg = None
with db.transaction() as db:
mailbox_create(username, db.root())
for m in db.root.messages[username]:
if m.msg_id == obj_uuid:
msg = m
break
if msg is None:
send_blank_response(conn, req, status_code=404)
return
else:
send_blank_response(conn, req,
payload=msg.to_dict(get_text=opts.get_text, get_attachments=opts.get_attachments))
def handle_message_get(req: Request, conn: PacketServerConnection, db: ZODB.DB): def handle_message_get(req: Request, conn: PacketServerConnection, db: ZODB.DB):
if re.match(since_regex,req.path): if 'id' in req.vars:
return handle_messages_since(req, conn, db)
elif 'id' in req.vars:
return handle_message_get_id(req, conn, db) return handle_message_get_id(req, conn, db)
if 'since' in req.vars:
return handle_messages_since(req, conn, db)
opts = parse_display_options(req) opts = parse_display_options(req)
username = ax25.Address(conn.remote_callsign).call.upper().strip() username = ax25.Address(conn.remote_callsign).call.upper().strip()
msg_return = [] msg_return = []
@@ -397,13 +442,16 @@ def handle_message_post(req: Request, conn: PacketServerConnection, db: ZODB.DB)
return return
msg.msg_from = username msg.msg_from = username
try: try:
send_counter, failed = msg.send(db) send_counter, failed, msg_id = msg.send(db)
except: except:
send_blank_response(conn, req, status_code=500) send_blank_response(conn, req, status_code=500)
logging.error(f"Error while attempting to send message:\n{format_exc()}") logging.error(f"Error while attempting to send message:\n{format_exc()}")
return return
send_blank_response(conn, req, status_code=201, payload={"successes": send_counter, "failed": failed}) send_blank_response(conn, req, status_code=201, payload={
"successes": send_counter,
"failed": failed,
'msg_id': msg_id})
def message_root_handler(req: Request, conn: PacketServerConnection, db: ZODB.DB): def message_root_handler(req: Request, conn: PacketServerConnection, db: ZODB.DB):
logging.debug(f"{req} being processed by message_root_handler") logging.debug(f"{req} being processed by message_root_handler")