Skip to content

Commit

Permalink
0.2.7 (#18)
Browse files Browse the repository at this point in the history
- Добавлена возможность указывать причину дисконекта с сервера
- Добавлена возможность поставить колбек при диконекте(колбек должен
принимать 1 параметр - `reason: str`)
- Для сервера добавлены следующие методы `set_timeout`,
`set_allow_registration`, `set_max_players`
- Исправлена ошибка при вызове ошибки при диконекте на винде (иди нахуй
винда, я ебал тебя)
  • Loading branch information
themanyfaceddemon authored Sep 22, 2024
1 parent d1a7fa5 commit 990e9ee
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 39 deletions.
57 changes: 45 additions & 12 deletions DMBotNetwork/main/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import uuid
from collections.abc import Callable
from pathlib import Path
from typing import (Any, Dict, List, Optional, Type, Union, get_args,
get_origin, get_type_hints)
from typing import (Any, Awaitable, Dict, List, Optional, Type, Union,
get_args, get_origin, get_type_hints)

import aiofiles

from .utils import ResponseCode
from .utils.states import ClientState

logger = logging.getLogger("DMBN:Client")

Expand All @@ -28,14 +29,17 @@ class Client:
_reader: Optional[asyncio.StreamReader] = None
_writer: Optional[asyncio.StreamWriter] = None

_is_auth: bool = False
_is_connected: bool = False
_state: int = ClientState.DISCONNECTED

_login: str = "owner"
_password: str = "owner_password"
_use_registration: bool = False
_content_path: Path = Path("")

_callback_on_disconect: Optional[
Callable[[Optional[str]], Awaitable[None]] | Callable[[Optional[str]], None]
] = None

@classmethod
def register_methods_from_class(cls, external_classes: Type | List[Type]) -> None:
"""Регистрация методов с префиксом 'net_' из внешнего класса."""
Expand Down Expand Up @@ -106,10 +110,12 @@ async def req_net_func(cls, func_name: str, **kwargs) -> None:
await cls.send_package(ResponseCode.NET_REQ, net_func_name=func_name, **kwargs)

@classmethod
async def req_get_data(cls, func_name: str, get_key: Optional[str], **kwargs) -> Any:
async def req_get_data(
cls, func_name: str, get_key: Optional[str], **kwargs
) -> Any:
if get_key is None:
get_key = str(uuid.uuid4())

if get_key in cls._data_cache:
return cls._data_cache.pop(get_key)

Expand All @@ -135,7 +141,7 @@ async def _handle_data_from_server(cls, get_key: str, data: Any) -> None:

@classmethod
def is_connected(cls) -> bool:
return cls._is_auth and cls._is_connected
return True if cls._state & ClientState.AUTHORIZED else False

@classmethod
def get_server_name(cls) -> str:
Expand All @@ -145,6 +151,15 @@ def get_server_name(cls) -> str:
def get_login(cls) -> str:
return cls._login

@classmethod
def set_callback_on_disconect(
cls,
value: Optional[
Callable[[Optional[str]], Awaitable[None]] | Callable[[Optional[str]], None]
] = None,
) -> None:
cls._callback_on_disconect = value

@classmethod
def setup(
cls, login: str, password: str, use_registration: bool, content_path: str | Path
Expand Down Expand Up @@ -176,9 +191,12 @@ def setup(

@classmethod
async def connect(cls, host, port) -> None:
if not cls._state & ClientState.DISCONNECTED:
raise RuntimeError("Already connected")

try:
cls._reader, cls._writer = await asyncio.open_connection(host, port)
cls._is_connected = True
cls._state = ClientState.CONNECTED

logger.info(f"Connected to {host}:{port}")

Expand All @@ -191,8 +209,7 @@ async def connect(cls, host, port) -> None:
@classmethod
async def disconnect(cls) -> None:
async with cls._disconnect_lock:
cls._is_connected = False
cls._is_auth = False
cls._state = ClientState.DISCONNECTED

if cls._writer:
try:
Expand Down Expand Up @@ -221,14 +238,19 @@ async def disconnect(cls) -> None:
@classmethod
async def _server_handler(cls) -> None:
try:
while cls._is_connected:
while not cls._state & ClientState.DISCONNECTED:
receive_package = await cls._receive_package()

code = receive_package.pop("code", None)
if not code:
logger.error(f"Receive data must has 'code' key: {receive_package}")
continue

if code == ResponseCode.DISCONNECT:
reason = receive_package.pop("reason", None)
await cls._on_disconect(reason)
break

if code == ResponseCode.NET_REQ:
await cls._call_func(
receive_package.pop("net_func_name", None),
Expand Down Expand Up @@ -267,6 +289,17 @@ async def _server_handler(cls) -> None:
finally:
await cls.disconnect()

@classmethod
async def _on_disconect(cls, reason: Optional[str] = None) -> None:
if cls._callback_on_disconect is None:
return

if inspect.iscoroutinefunction(cls._callback_on_disconect):
await cls._callback_on_disconect(reason)

else:
cls._callback_on_disconect(reason)

@classmethod
def _log_handler(cls, code: int, receive_package: dict) -> None:
message = receive_package.get("message", None)
Expand Down Expand Up @@ -303,7 +336,7 @@ async def _auth_handler(cls, code: int, receive_package: dict) -> None:
if not server_name:
return

cls._is_auth = True
cls._state = ClientState.AUTHORIZED
cls._server_name = server_name

@classmethod
Expand Down
61 changes: 36 additions & 25 deletions DMBotNetwork/main/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Server:

_is_online: bool = False

_server_name: str = "Dev_Server"
_server_name: str = "dev"
_allow_registration: bool = True
_timeout: float = 30.0
_max_players: int = -1
Expand Down Expand Up @@ -109,13 +109,25 @@ async def setup_server(
cls._server = await asyncio.start_server(cls._cl_handler, host, port)
logger.info(f"Server setup. Host: {host}, port:{port}.")

@classmethod
def set_timeout(cls, value: float) -> None:
cls._timeout = value

@classmethod
def set_allow_registration(cls, value: bool) -> None:
cls._allow_registration = value

@classmethod
def set_max_players(cls, value: int) -> None:
cls._max_players = value

@classmethod
async def start(cls) -> None:
if not cls._server:
raise RuntimeError("Server is not initialized.")
raise RuntimeError("Server is not initialized")

if cls._is_online:
raise RuntimeError("Server already start.")
raise RuntimeError("Server already active")

await ServerDB.start()

Expand All @@ -138,12 +150,15 @@ async def start(cls) -> None:
@classmethod
async def stop(cls) -> None:
if not cls._is_online:
raise RuntimeError("Server is not working.")
raise RuntimeError("Server is inactive")

cls._is_online = False

await asyncio.gather(
*(cl_unit.disconnect() for cl_unit in cls._cl_units.values())
*(
cl_unit.disconnect("Server shutdown")
for cl_unit in cls._cl_units.values()
)
)
cls._cl_units.clear()

Expand Down Expand Up @@ -175,45 +190,40 @@ async def _cl_handler(
cl_unit = ClUnit("init", reader, writer)

if not cls._is_online:
await cl_unit.send_log_error("Server is shutdown")
await cl_unit.disconnect("Server is inactive")
return

try:
await cls._auth(cl_unit)

except TimeoutError:
await cl_unit.send_log_error("Timeout for auth.")
await cl_unit.disconnect()
await cl_unit.disconnect("Timeout for auth")
return

except ValueError as err:
await cl_unit.send_log_error(str(err))
await cl_unit.disconnect()
await cl_unit.disconnect(str(err))
return

except Exception as err:
await cl_unit.send_log_error(f"An unexpected error occurred: {err}")
await cl_unit.disconnect()
await cl_unit.disconnect(f"An unexpected error occurred: {err}")
return

async with cls._cl_units_lock:
cls._cl_units[cl_unit.login] = cl_unit

logger.info(f"{cl_unit.login} is connected.")
logger.info(f"{cl_unit.login} is connected")

try:
while cls._is_online:
try:
receive_package = await cl_unit.receive_package()
if not isinstance(receive_package, dict):
await cl_unit.send_log_error("Receive data type expected dict.")
await cl_unit.send_log_error("Receive data type expected dict")
continue

code = receive_package.pop("code", None)
if not code:
await cl_unit.send_log_error(
"Receive data must has 'code' key."
)
await cl_unit.send_log_error("Receive data must has 'code' key")
continue

if code == ResponseCode.NET_REQ:
Expand All @@ -240,7 +250,7 @@ async def _cl_handler(
)

else:
await cl_unit.send_log_error("Unknown 'code' for net type.")
await cl_unit.send_log_error("Unknown 'code' for net type")

except PermissionError as err:
await cl_unit.send_log_error(
Expand All @@ -252,6 +262,7 @@ async def _cl_handler(
ConnectionAbortedError,
asyncio.exceptions.IncompleteReadError,
ConnectionResetError,
OSError,
):
pass

Expand All @@ -264,38 +275,38 @@ async def _cl_handler(
cls._cl_units.pop(cl_unit.login, None)

await cl_unit.disconnect()
logger.info(f"{cl_unit.login} is disconected.")
logger.info(f"{cl_unit.login} is disconected")

@classmethod
async def _auth(cls, cl_unit: ClUnit) -> None:
if cls._max_players != -1 and cls._max_players <= len(cls._cl_units):
raise ValueError("Server is full.")
raise ValueError("Server is full")

await cl_unit.send_package(ResponseCode.AUTH_REQ)
receive_package = await asyncio.wait_for(
cl_unit.receive_package(), cls._timeout
)

if not isinstance(receive_package, dict):
raise ValueError("Receive data type expected dict.")
raise ValueError("Receive data type expected dict")

code = receive_package.get("code", None)
if not code:
raise ValueError("Receive data must has 'code' key.")
raise ValueError("Receive data must has 'code' key")

code = ResponseCode(code)

if not ResponseCode.is_client_auth(code):
raise ValueError("Unknown 'code' for auth type.")
raise ValueError("Unknown 'code' for auth type")

login = receive_package.get("login", None)
password = receive_package.get("password", None)
if not all([login, password]):
raise ValueError("Receive data must has 'login' and 'password' keys.")
raise ValueError("Receive data must has 'login' and 'password' keys")

if code == ResponseCode.AUTH_ANS_REGIS:
if not cls._allow_registration:
raise ValueError("Registration is not allowed.")
raise ValueError("Registration is not allowed")

await ServerDB.add_user(login, password)
cl_unit.login = login
Expand Down
3 changes: 2 additions & 1 deletion DMBotNetwork/main/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .cl_unit import ClUnit
from .response_code import ResponseCode
from .server_db import ServerDB
from .states import ClientState

__all__ = ["ClUnit", "ResponseCode", "ServerDB"]
__all__ = ["ClUnit", "ResponseCode", "ServerDB", "ClientState"]
6 changes: 5 additions & 1 deletion DMBotNetwork/main/utils/cl_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import base64
import json
from pathlib import Path
from typing import Optional

import aiofiles

Expand Down Expand Up @@ -197,8 +198,11 @@ async def _receive_raw_data(self) -> bytes:

return await self._reader.readexactly(data_length)

async def disconnect(self) -> None:
async def disconnect(self, reason: Optional[str] = None) -> None:
"""Отключение соединения."""
if reason is not None:
await self.send_package(ResponseCode.DISCONNECT, reason=reason)

if self._writer:
try:
self._writer.close()
Expand Down
1 change: 1 addition & 0 deletions DMBotNetwork/main/utils/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .cl_unit import ClUnit
from .server_db import ServerDB


def require_access(req_access: List[str] | str):
"""
A decorator that ensures the user has the required access level(s) before executing the function.
Expand Down
3 changes: 3 additions & 0 deletions DMBotNetwork/main/utils/response_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@


class ResponseCode(IntEnum):
# Системные члены
DISCONNECT = 0

# Авторизация
AUTH_REQ = 10 # Запрос авторизации от сервера
AUTH_ANS_LOGIN = 11 # Клиент отправляет запрос на авторизацию
Expand Down
4 changes: 4 additions & 0 deletions DMBotNetwork/main/utils/states.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class ClientState:
DISCONNECTED: int = 0b001
CONNECTED: int = 0b010
AUTHORIZED: int = 0b100

0 comments on commit 990e9ee

Please sign in to comment.