Merge pull request #5 from anorak999/fix/new-change

This commit is contained in:
mirai 2025-11-07 16:45:09 +03:00 committed by GitHub
commit 945d7aeb62
9 changed files with 43 additions and 31 deletions

View File

@ -40,7 +40,7 @@ class Client(RSAService, RichClientRenderer):
return f"{self.ws_url}{path}" return f"{self.ws_url}{path}"
def _connect_ws(self, path: str, retries: int = 5, backoff: float = 0.5): 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): for attempt in range(retries):
try: try:
return create_connection(self._ws_full(path)) return create_connection(self._ws_full(path))

View File

@ -19,7 +19,7 @@ class CryptoService(ABC):
raise NotImplementedError("Need to implement generate keys method") raise NotImplementedError("Need to implement generate keys method")
@abstractmethod @abstractmethod
def _get_generated_keys(self) -> list[str]: def _get_generated_keys(self) -> tuple:
raise NotImplementedError("Need to implement get generated keys method") raise NotImplementedError("Need to implement get generated keys method")
@abstractmethod @abstractmethod

View File

@ -2,23 +2,33 @@ from abc import ABC, abstractmethod
class ClientRenderer(ABC): 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 @abstractmethod
def print_message(self, message: str) -> str: def print_message(self, message: str) -> str:
raise NotImplementedError("Need to implement print_message") raise NotImplementedError("Need to implement print_message")
@abstractmethod @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") raise NotImplementedError("Need to implement clear_console")
@abstractmethod @abstractmethod
def print_ip(self, url: str, username: str): def print_ip(self, ip: str) -> str:
raise NotImplementedError("Need to implement print_ip") raise NotImplementedError("Need to implement print_ip")
@abstractmethod @abstractmethod
def print_username(self): def print_username(self, username: str) -> str:
raise NotImplementedError("Need to implement print_username") raise NotImplementedError("Need to implement print_username")
@abstractmethod @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") raise NotImplementedError("Need to implement print_chat")

View File

@ -28,7 +28,8 @@ class RSAService(CryptoService):
stream=True, stream=True,
) )
r.raise_for_status() 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.symmetric_key = rsa.decrypt(message, self.private_key)
self.fernet = Fernet(self.symmetric_key) self.fernet = Fernet(self.symmetric_key)

View File

@ -21,10 +21,11 @@ class DefaultClientRenderer(ClientRenderer):
def print_message(self, message: str) -> str: def print_message(self, message: str) -> str:
""" generating string with message in required format """ generating string with message in required format
""" """
message = message.split(":") # split only on the first ':' to keep message contents intact
if message[0] == self.username: parts = message.split(":", 1)
return COLORS["my_username_color"] + message[0] + ": " + message[1] + COLORS["text_color"] if parts[0] == self.username:
return message[0] + ": " + message[1] + COLORS["text_color"] return COLORS["my_username_color"] + parts[0] + ": " + parts[1] + COLORS["text_color"]
return parts[0] + ": " + parts[1] + COLORS["text_color"]
def clear_console(self): def clear_console(self):
# For windows clear command its cls # For windows clear command its cls
@ -44,9 +45,10 @@ class DefaultClientRenderer(ClientRenderer):
self, self,
username: str username: str
) -> 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"]): for i, msg in enumerate(response["messages"]):
actual_message = self._decrypt(msg) actual_message = self._decrypt(msg)
if i == 0: if i == 0:

View File

@ -25,16 +25,17 @@ class RichClientRenderer(ClientRenderer):
def print_message(self, message: str) -> Text: def print_message(self, message: str) -> Text:
""" generating string with message in required format """ generating string with message in required format
""" """
message = message.split(":") # split only on the first ':' so message bodies containing ':' are preserved
if message[0] == self.username: parts = message.split(":", 1)
if parts[0] == self.username:
return \ return \
Text(text=message[0], style="bold") + \ Text(text=parts[0], style="bold") + \
Text(text=": ", style="bold") + \ Text(text=": ", style="bold") + \
Text(text=message[1], style="underline") Text(text=parts[1], style="underline")
return \ return \
Text(text=message[0], style="bold") + \ Text(text=parts[0], style="bold") + \
Text(text=": ", style="bold") + \ Text(text=": ", style="bold") + \
Text(text=message[1], style="underline") Text(text=parts[1], style="underline")
def clear_console(self): def clear_console(self):
# For windows clear command its cls # For windows clear command its cls
@ -56,7 +57,7 @@ class RichClientRenderer(ClientRenderer):
) -> str: ) -> str:
return username return username
def print_chat(self, response: list[str]) -> str: def print_chat(self, response) -> None:
self.clear_console() self.clear_console()
for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]):
actual_message = self._decrypt(msg) actual_message = self._decrypt(msg)

View File

@ -40,7 +40,10 @@ def attach_endpoints(app: Sanic):
while True: while True:
serialized_message: dict = await _get_bytes_and_serialize(ws) serialized_message: dict = await _get_bytes_and_serialize(ws)
await _check_ws_for_close_status(serialized_message, 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) MESSAGES_MEMORY_DB.append(new_message)
await ws.send(str({"status": "ok"})) await ws.send(str({"status": "ok"}))
await asyncio.sleep(0.2) await asyncio.sleep(0.2)

View File

@ -25,7 +25,7 @@ async def _generate_new_message(
async def _generate_update_payload( async def _generate_update_payload(
memory_msgs: list[str], memory_msgs: list[Message],
users_structure: dict users_structure: dict
) -> str: ) -> str:
return str({ return str({

View File

@ -8,13 +8,8 @@ setuptools.setup(
version="1.1.22", version="1.1.22",
author="dinosaurtirex", author="dinosaurtirex",
author_email="sneakybeaky18@gmail.com", author_email="sneakybeaky18@gmail.com",
packages=[ # Use find_packages to correctly discover package names
"cmd_chat", packages=setuptools.find_packages(exclude=("tests", "docs")),
"cmd_chat/client",
"cmd_chat/client/core",
"cmd_chat/client/core/abs",
"cmd_chat/server"
],
description="Secured console chat with RSA & Fernet", description="Secured console chat with RSA & Fernet",
long_description=description, long_description=description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",