Messages client done. Messages server improved slightly.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user