diff --git a/src/packetserver/client/messages.py b/src/packetserver/client/messages.py index f8bc663..b78e202 100644 --- a/src/packetserver/client/messages.py +++ b/src/packetserver/client/messages.py @@ -2,33 +2,190 @@ import datetime from packetserver.client import Client from packetserver.common import Request, Response, PacketServerConnection +from packetserver.common.util import to_date_digits from typing import Union, Optional from uuid import UUID, uuid4 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"" + + @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: - # TODO MessageWrapper 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(): - raise ValueError("Data dict was not an object dictionary.") + raise ValueError("Data dict was not a message dictionary.") self.data = data + @property + def text(self) -> str: + return self.data['text'] -def send_message(client: Client, bbs_callsign: str,): - # TODO send message - pass + @property + def sent(self) -> datetime.datetime: + return datetime.datetime.fromisoformat(self.data['sent_at']) -def get_message_uuid(): - # TODO get message by uuid - pass + @property + def msg_id(self) -> UUID: + return UUID(self.data['id']) -def get_messages_since(): - # TODO get messages since date - pass + @property + def from_user(self) -> str: + return self.data['from'] -def get_messages(): - # TODO get messages default - pass \ No newline at end of file + @property + def to_users(self) -> list[str]: + 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"" + + 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 diff --git a/src/packetserver/server/messages.py b/src/packetserver/server/messages.py index 3b02870..ae33b68 100644 --- a/src/packetserver/server/messages.py +++ b/src/packetserver/server/messages.py @@ -15,6 +15,7 @@ import uuid from uuid import UUID from packetserver.common.util import email_valid from packetserver.server.objects import Object +from packetserver.server.users import User from BTrees.OOBTree import TreeSet from packetserver.server.users import User, user_authorized from traceback import format_exc @@ -25,9 +26,15 @@ since_regex = """^message\\/since\\/(\\d+)$""" def mailbox_create(username: str, db_root: PersistentMapping): 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']: 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() @@ -97,6 +104,7 @@ class Attachment: "name": self.name, "binary": self.binary, "size_bytes": self.size, + "data": b'' } if include_data: d['data'] = self.data @@ -200,7 +208,9 @@ class Message(persistent.Persistent): new_attachments = [] for i in self.attachments: 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: new_attachments.append(i) send_counter = 0 @@ -208,6 +218,8 @@ class Message(persistent.Persistent): failed = [] to_all = False 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: recipient = recipient.upper().strip() if recipient is None: @@ -217,11 +229,13 @@ class Message(persistent.Persistent): to_all = True break 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: 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: @@ -231,8 +245,10 @@ class Message(persistent.Persistent): except: logging.error(f"Error sending message to {recipient}:\n{format_exc()}") failed.append(recipient) - - return send_counter, failed + self.msg_delivered = True + 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', '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}.") return try: - since_date = from_date_digits(req.path.split("/")[2]) + since_date = from_date_digits(req.vars['since']) except ValueError as v: send_blank_response(conn, req, 400, "invalid date string") return @@ -347,14 +363,43 @@ def handle_messages_since(req: Request, conn: PacketServerConnection, db: ZODB.D send_response(conn, response, req) def handle_message_get_id(req: Request, conn: PacketServerConnection, db: ZODB.DB): - # TODO message get specific by uuid - pass + 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 + 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): - if re.match(since_regex,req.path): - return handle_messages_since(req, conn, db) - elif 'id' in req.vars: + if 'id' in req.vars: 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) username = ax25.Address(conn.remote_callsign).call.upper().strip() msg_return = [] @@ -397,13 +442,16 @@ def handle_message_post(req: Request, conn: PacketServerConnection, db: ZODB.DB) return msg.msg_from = username try: - send_counter, failed = msg.send(db) + send_counter, failed, msg_id = msg.send(db) except: send_blank_response(conn, req, status_code=500) logging.error(f"Error while attempting to send message:\n{format_exc()}") 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): logging.debug(f"{req} being processed by message_root_handler")