From d084bbc73d7ff7071e83bec2dc98e28322ca5bb3 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 29 Sep 2024 22:59:37 +1300 Subject: [PATCH] implement sending messages to group --- database.py | 16 ++++++++ meshchat.py | 34 ++++++++++++++++ src/backend/group_chat/group_chat_client.py | 8 ++++ src/backend/group_chat/group_chat_server.py | 45 +++++++++++++++++++-- 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/database.py b/database.py index 9307658..3e8f7e0 100644 --- a/database.py +++ b/database.py @@ -158,3 +158,19 @@ class GroupMember(BaseModel): # only allow a single row per group_destination_hash/member_identity_hash pair SQL('UNIQUE (group_destination_hash, member_identity_hash)'), ] + + +class GroupMessage(BaseModel): + + id = BigAutoField() + hash = CharField(unique=True) + group_destination_hash = CharField(index=True) + member_identity_hash = CharField() + content = TextField() + + created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) + updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) + + # define table name + class Meta: + table_name = "group_messages" diff --git a/meshchat.py b/meshchat.py index 1d7e0d6..580c8b1 100644 --- a/meshchat.py +++ b/meshchat.py @@ -139,6 +139,39 @@ class GroupChatDataProvider(GroupDataProviderInterface): return members + # save a message sent to the group by the provided identity + def on_message_received(self, group_destination_hash: bytes, identity_hash: bytes, data: dict): + + # find group + group = self.find_group(group_destination_hash) + if group is None: + raise Exception("Group not found") + + # generate a message hash similar to lxmf + message_data_to_hash = b"" + message_data_to_hash += group_destination_hash + message_data_to_hash += identity_hash + message_data_to_hash += msgpack.packb(data) + message_hash = RNS.Identity.full_hash(message_data_to_hash).hex() + + # get content as string + content = None + if "content" in data: + content = str(data["content"]) + + # prepare data to insert or update + data = { + "hash": message_hash, + "group_destination_hash": group.destination_hash, + "member_identity_hash": identity_hash.hex(), + "content": content, + "updated_at": datetime.now(timezone.utc), + } + + # upsert message to database + database.GroupMessage.insert(data).on_conflict(conflict_target=[database.GroupMessage.hash], update=data).execute() + + class ReticulumMeshChat: def __init__(self, identity: RNS.Identity, storage_dir, reticulum_config_dir): @@ -175,6 +208,7 @@ class ReticulumMeshChat: database.CustomDestinationDisplayName, database.Group, database.GroupMember, + database.GroupMessage, database.LxmfMessage, database.LxmfConversationReadState, ]) diff --git a/src/backend/group_chat/group_chat_client.py b/src/backend/group_chat/group_chat_client.py index 571feeb..60b9581 100644 --- a/src/backend/group_chat/group_chat_client.py +++ b/src/backend/group_chat/group_chat_client.py @@ -103,6 +103,13 @@ class GroupChatClient: "limit": limit, }).encode("utf-8")) + # send message + async def send_message(self, content: str): + return await self.request("/api/v1/messages/send", data=json.dumps({ + "timestamp": time.time(), + "content": content, + }).encode("utf-8")) + # python3 group_chat_client.py # used for testing group chat client @@ -120,6 +127,7 @@ async def main(): print(await group_chat_client.join("Test Display Name")) print(await group_chat_client.get_info()) print(await group_chat_client.get_members(page=1, limit=10)) + print(await group_chat_client.send_message("hello world!")) print(await group_chat_client.leave()) print(await group_chat_client.get_info()) diff --git a/src/backend/group_chat/group_chat_server.py b/src/backend/group_chat/group_chat_server.py index 87cd45f..02b53e1 100644 --- a/src/backend/group_chat/group_chat_server.py +++ b/src/backend/group_chat/group_chat_server.py @@ -27,6 +27,11 @@ class GroupDataProviderInterface: def get_members(self, group_destination_hash: bytes, page: int | None, limit: int | None): raise Exception("Not Implemented") + # save a message sent to the group by the provided identity + def on_message_received(self, group_destination_hash: bytes, identity_hash: bytes, data: dict): + raise Exception("Not Implemented") + + # a group chat server than can handle membership management class GroupChatServer: @@ -53,6 +58,7 @@ class GroupChatServer: self.group_destination.register_request_handler(path="/api/v1/join", response_generator=self.on_received_api_v1_join_request, allow=RNS.Destination.ALLOW_ALL) self.group_destination.register_request_handler(path="/api/v1/leave", response_generator=self.on_received_api_v1_leave_request, allow=RNS.Destination.ALLOW_ALL) self.group_destination.register_request_handler(path="/api/v1/members", response_generator=self.on_received_api_v1_members_request, allow=RNS.Destination.ALLOW_ALL) + self.group_destination.register_request_handler(path="/api/v1/messages/send", response_generator=self.on_received_api_v1_messages_send_request, allow=RNS.Destination.ALLOW_ALL) # announce group destination def announce(self): @@ -79,6 +85,10 @@ class GroupChatServer: def identity_not_provided_error_response(self): return self.error_response("You must identity to to access this endpoint.") + # error response for failing to parse request data as json + def request_json_parsing_error_response(self): + return self.error_response("Failed to parse request data as JSON.") + # /api/v1/info def on_received_api_v1_info_request(self, path, data, request_id, remote_identity, requested_at): return json.dumps({ @@ -101,8 +111,7 @@ class GroupChatServer: json_data = json.loads(data.decode("utf-8")) display_name = json_data["display_name"] or "Anonymous Peer" except: - print("failed to parse request data as json") - pass + return self.request_json_parsing_error_response() # ensure user is not already a member if self.data_provider.is_member(self.group_destination.hash, remote_identity.hash): @@ -141,8 +150,7 @@ class GroupChatServer: page = json_data["page"] limit = json_data["limit"] except: - print("failed to parse request data as json") - pass + return self.request_json_parsing_error_response() # ensure user is a member if not self.data_provider.is_member(self.group_destination.hash, remote_identity.hash): @@ -162,3 +170,32 @@ class GroupChatServer: return json.dumps({ "members": group_members, }).encode("utf-8") + + # /api/v1/messages/send + def on_received_api_v1_messages_send_request(self, path, data: bytes | None, request_id, remote_identity: RNS.Identity | None, requested_at): + + # ensure user has identified + if remote_identity is None: + return self.identity_not_provided_error_response() + + # ensure user is a member + if not self.data_provider.is_member(self.group_destination.hash, remote_identity.hash): + return self.error_response("You are not a member of this group") + + # attempt to parse data as json + json_data = None + if data is not None: + try: + json_data = json.loads(data.decode("utf-8")) + except: + return self.request_json_parsing_error_response() + + # todo ensure only expected content is received + # todo ensure timestamp + + # handle received message + self.data_provider.on_message_received(self.group_destination.hash, remote_identity.hash, json_data) + + return json.dumps({ + "success": "Message received", + }).encode("utf-8")