diff --git a/cmd_chat/client/client.py b/cmd_chat/client/client.py index 11f795b..b50bdc9 100644 --- a/cmd_chat/client/client.py +++ b/cmd_chat/client/client.py @@ -40,7 +40,7 @@ class Client(RSAService, RichClientRenderer): return f"{self.ws_url}{path}" def _connect_ws(self, path: str, retries: int = 5, backoff: float = 0.5): - last_exc = None + last_exc: Exception = ConnectionError("Failed to connect") for attempt in range(retries): try: return create_connection(self._ws_full(path)) diff --git a/cmd_chat/client/core/abs/abs_crypto.py b/cmd_chat/client/core/abs/abs_crypto.py index f82f4ba..e9ab4fb 100644 --- a/cmd_chat/client/core/abs/abs_crypto.py +++ b/cmd_chat/client/core/abs/abs_crypto.py @@ -19,7 +19,7 @@ class CryptoService(ABC): raise NotImplementedError("Need to implement generate keys method") @abstractmethod - def _get_generated_keys(self) -> list[str]: + def _get_generated_keys(self) -> tuple: raise NotImplementedError("Need to implement get generated keys method") @abstractmethod diff --git a/cmd_chat/client/core/abs/abs_renderer.py b/cmd_chat/client/core/abs/abs_renderer.py index eb04191..38ef96e 100644 --- a/cmd_chat/client/core/abs/abs_renderer.py +++ b/cmd_chat/client/core/abs/abs_renderer.py @@ -2,23 +2,33 @@ from abc import ABC, abstractmethod class ClientRenderer(ABC): + # These attributes are expected to be provided by subclasses + # (typically via multiple inheritance with CryptoService) + username: str + + @abstractmethod + def _decrypt(self, message: str) -> str: + """Decrypt an encrypted message (provided by crypto mixin).""" + raise NotImplementedError("Need to implement _decrypt") @abstractmethod def print_message(self, message: str) -> str: raise NotImplementedError("Need to implement print_message") @abstractmethod - def clear_console(self, message: str) -> str: + def clear_console(self) -> None: + """Clear the client console (platform-specific).""" raise NotImplementedError("Need to implement clear_console") @abstractmethod - def print_ip(self, url: str, username: str): + def print_ip(self, ip: str) -> str: raise NotImplementedError("Need to implement print_ip") @abstractmethod - def print_username(self): + def print_username(self, username: str) -> str: raise NotImplementedError("Need to implement print_username") @abstractmethod - def print_chat(self) -> list[str]: + def print_chat(self, response) -> None: + """Render chat payload (response is expected to be a mapping with 'messages' and 'users_in_chat').""" raise NotImplementedError("Need to implement print_chat") diff --git a/cmd_chat/client/core/crypto.py b/cmd_chat/client/core/crypto.py index fa87b6c..976e7ab 100644 --- a/cmd_chat/client/core/crypto.py +++ b/cmd_chat/client/core/crypto.py @@ -28,7 +28,8 @@ class RSAService(CryptoService): stream=True, ) r.raise_for_status() - message = r.raw.read(999) + # read the full response content (server returns encrypted symmetric key) + message = r.content self.symmetric_key = rsa.decrypt(message, self.private_key) self.fernet = Fernet(self.symmetric_key) diff --git a/cmd_chat/client/core/default_renderer.py b/cmd_chat/client/core/default_renderer.py index f6a130c..765bdee 100644 --- a/cmd_chat/client/core/default_renderer.py +++ b/cmd_chat/client/core/default_renderer.py @@ -21,10 +21,11 @@ class DefaultClientRenderer(ClientRenderer): def print_message(self, message: str) -> str: """ generating string with message in required format """ - message = message.split(":") - if message[0] == self.username: - return COLORS["my_username_color"] + message[0] + ": " + message[1] + COLORS["text_color"] - return message[0] + ": " + message[1] + COLORS["text_color"] + # split only on the first ':' to keep message contents intact + parts = message.split(":", 1) + if parts[0] == self.username: + return COLORS["my_username_color"] + parts[0] + ": " + parts[1] + COLORS["text_color"] + return parts[0] + ": " + parts[1] + COLORS["text_color"] def clear_console(self): # For windows clear command its cls @@ -44,9 +45,10 @@ class DefaultClientRenderer(ClientRenderer): self, username: str ) -> str: - return f"USERNAME: " + COLORS["ip_color"] + username + COLORS["username_color"] + # Username label + colored username + return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] - def print_chat(self, response: list[str]) -> str: + def print_chat(self, response) -> None: for i, msg in enumerate(response["messages"]): actual_message = self._decrypt(msg) if i == 0: diff --git a/cmd_chat/client/core/rich_renderer.py b/cmd_chat/client/core/rich_renderer.py index 246eb2b..0f40c3c 100644 --- a/cmd_chat/client/core/rich_renderer.py +++ b/cmd_chat/client/core/rich_renderer.py @@ -25,16 +25,17 @@ class RichClientRenderer(ClientRenderer): def print_message(self, message: str) -> Text: """ generating string with message in required format """ - message = message.split(":") - if message[0] == self.username: + # split only on the first ':' so message bodies containing ':' are preserved + parts = message.split(":", 1) + if parts[0] == self.username: return \ - Text(text=message[0], style="bold") + \ + Text(text=parts[0], style="bold") + \ Text(text=": ", style="bold") + \ - Text(text=message[1], style="underline") + Text(text=parts[1], style="underline") return \ - Text(text=message[0], style="bold") + \ + Text(text=parts[0], style="bold") + \ Text(text=": ", style="bold") + \ - Text(text=message[1], style="underline") + Text(text=parts[1], style="underline") def clear_console(self): # For windows clear command its cls @@ -56,7 +57,7 @@ class RichClientRenderer(ClientRenderer): ) -> str: return username - def print_chat(self, response: list[str]) -> str: + def print_chat(self, response) -> None: self.clear_console() for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): actual_message = self._decrypt(msg) diff --git a/cmd_chat/server/server.py b/cmd_chat/server/server.py index 2d05239..33bbe36 100644 --- a/cmd_chat/server/server.py +++ b/cmd_chat/server/server.py @@ -40,7 +40,10 @@ def attach_endpoints(app: Sanic): while True: serialized_message: dict = await _get_bytes_and_serialize(ws) await _check_ws_for_close_status(serialized_message, ws) - new_message = await _generate_new_message(serialized_message.get("text")) + text = serialized_message.get("text") + if text is None: + continue + new_message = await _generate_new_message(text) MESSAGES_MEMORY_DB.append(new_message) await ws.send(str({"status": "ok"})) await asyncio.sleep(0.2) diff --git a/cmd_chat/server/services.py b/cmd_chat/server/services.py index ec3280c..e951d32 100644 --- a/cmd_chat/server/services.py +++ b/cmd_chat/server/services.py @@ -25,7 +25,7 @@ async def _generate_new_message( async def _generate_update_payload( - memory_msgs: list[str], + memory_msgs: list[Message], users_structure: dict ) -> str: return str({ diff --git a/setup.py b/setup.py index abe0895..7eef936 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,15 @@ import setuptools - + with open("README.md", "r", encoding="utf-8") as fh: description = fh.read() - + setuptools.setup( name="secured_console_chat", version="1.1.22", author="dinosaurtirex", author_email="sneakybeaky18@gmail.com", - packages=[ - "cmd_chat", - "cmd_chat/client", - "cmd_chat/client/core", - "cmd_chat/client/core/abs", - "cmd_chat/server" - ], + # Use find_packages to correctly discover package names + packages=setuptools.find_packages(exclude=("tests", "docs")), description="Secured console chat with RSA & Fernet", long_description=description, long_description_content_type="text/markdown",