feat: Добавлена обработка сообщений, ивентов и подписок
This commit is contained in:
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
from .constants import Constant
|
|
||||||
|
|
||||||
from .classes.daedalus import Daedalus
|
|
||||||
from .classes.damage import DamageDescription
|
|
||||||
from .classes.items import ItemGround
|
|
||||||
from .classes.items import ItemsGround
|
|
||||||
from .classes.mds import Mds
|
|
||||||
from .classes.sky import Sky
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import inspect
|
||||||
|
|
||||||
|
_EVENTS = {}
|
||||||
|
|
||||||
|
def event(event_name: str, priority: int = 9999):
|
||||||
|
def inlineEvent(func):
|
||||||
|
if not inspect.iscoroutinefunction(func):
|
||||||
|
raise TypeError(f'Декоратор event поддерживает только подпрограммы')
|
||||||
|
if event_name not in _EVENTS:
|
||||||
|
_EVENTS[event_name] = []
|
||||||
|
|
||||||
|
_EVENTS[event_name].append({'function': func, 'priority': priority})
|
||||||
|
_EVENTS[event_name].sort(key = lambda x: x['priority'])
|
||||||
|
|
||||||
|
return func
|
||||||
|
return inlineEvent
|
||||||
|
|
||||||
|
async def call_event(event_name: str, *args, **kwargs):
|
||||||
|
if event_name not in _EVENTS:
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in _EVENTS[event_name]:
|
||||||
|
await item['function'](*args, **kwargs)
|
||||||
|
|||||||
@@ -2,57 +2,58 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from weakref import WeakValueDictionary
|
from weakref import WeakValueDictionary, WeakSet, finalize
|
||||||
from fastapi import WebSocket, FastAPI, Depends, HTTPException, WebSocketDisconnect, WebSocketException
|
from collections import UserDict
|
||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from fastapi import WebSocket, FastAPI, WebSocketDisconnect, WebSocketException
|
||||||
|
from .event import call_event
|
||||||
|
|
||||||
|
class TopicWeakDict(UserDict):
|
||||||
|
# Аналог WeakValueDict, но с сильными ссылками для возможности поддержки WeakSet
|
||||||
|
def __init__(self, initial_data = None):
|
||||||
|
super().__init__(initial_data or {})
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
finalize(value, lambda k=key, s=self: s.pop(k))
|
||||||
|
super().__setitem__(key, value)
|
||||||
|
|
||||||
class Server:
|
class Server:
|
||||||
|
_logger: logging.Logger = logging.getLogger(__name__)
|
||||||
_current_server: Server | None = None
|
_static_tokens: list[str] = []
|
||||||
|
_temp_tokens: list[str] = []
|
||||||
|
_requests: WeakValueDictionary[str, asyncio.Future] = WeakValueDictionary()
|
||||||
|
_topics = TopicWeakDict()
|
||||||
|
_topic_lock = asyncio.Lock()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_current_server(cls) -> Server:
|
def init(cls, *, app: FastAPI, static_tokens: list[str] = []):
|
||||||
if cls._current_server is None:
|
cls._logger.addHandler(logging.NullHandler())
|
||||||
raise ConnectionError('PyG2O сервер не подключен')
|
cls._static_tokens = static_tokens
|
||||||
|
|
||||||
return cls._current_server
|
cls._requests: WeakValueDictionary[str, asyncio.Future] = WeakValueDictionary()
|
||||||
|
cls._register_routes(app)
|
||||||
def __init__(self, *, app: FastAPI, server_username: str, server_password: str, client_password: str):
|
|
||||||
Server._current_server = self
|
|
||||||
self._security = HTTPBasic()
|
|
||||||
self._server_token: str = ''
|
|
||||||
self._server_username = server_username
|
|
||||||
self._server_password = server_password
|
|
||||||
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
self._logger.addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
self._server_connection: WebSocket | None = None
|
|
||||||
self._requests: WeakValueDictionary[str, asyncio.Future] = WeakValueDictionary()
|
|
||||||
self._register_routes(app)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def server_call(cls, message: str):
|
async def publish(cls, topic: str, message: str) -> asyncio.Future:
|
||||||
return await cls.get_current_server()._call(cls.get_current_server()._server_connection, message)
|
if topic not in cls._topics:
|
||||||
|
raise KeyError('Клиентов прослушивающих этот топик не существует')
|
||||||
|
|
||||||
async def _call(self, socket: WebSocket | None, message: str):
|
request, data = cls._make_request()
|
||||||
if socket is None:
|
|
||||||
raise ConnectionError('PyG2O сервер не подключен')
|
|
||||||
|
|
||||||
request, data = self._make_request()
|
|
||||||
data['data'] = message
|
data['data'] = message
|
||||||
data = json.dumps(data)
|
data = json.dumps(data)
|
||||||
# Меняем синтаксис под Squirrel
|
# Меняем синтаксис под Squirrel
|
||||||
data = data.replace("'", '\\"').replace('True', 'true').replace('False', 'false')
|
data = data.replace("'", '\\"').replace('True', 'true').replace('False', 'false')
|
||||||
|
|
||||||
await socket.send_text(message)
|
for connection in cls._topics[topic]:
|
||||||
|
await connection.send_text(data)
|
||||||
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
def _make_request(self):
|
@classmethod
|
||||||
|
def _make_request(cls):
|
||||||
request_id = str(uuid4())
|
request_id = str(uuid4())
|
||||||
request = asyncio.Future()
|
request = asyncio.Future()
|
||||||
self._requests[request_id] = request
|
cls._requests[request_id] = request
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'uuid': request_id,
|
'uuid': request_id,
|
||||||
@@ -61,57 +62,90 @@ class Server:
|
|||||||
|
|
||||||
return request, data
|
return request, data
|
||||||
|
|
||||||
def _register_routes(self, app):
|
@classmethod
|
||||||
@app.websocket('/pyg2o/server')
|
def _register_routes(cls, app):
|
||||||
async def pyg2o_main(websocket: WebSocket):
|
@app.websocket('/pyg2o')
|
||||||
await self._handle_server_connection(websocket)
|
async def pyg2o(websocket: WebSocket):
|
||||||
|
await cls._handle_connection(websocket)
|
||||||
|
|
||||||
@app.websocket('/pyg2o/client/{playerid}')
|
_ = pyg2o
|
||||||
async def pyg2o_client(websocket: WebSocket, playerid: int):
|
|
||||||
await self._handle_client_connection(websocket, playerid)
|
|
||||||
|
|
||||||
# Я потратил примерно 2ч чтобы понять, почему pyright игнорирует type: ignore
|
@classmethod
|
||||||
# Я сдаюсь, мне пришлось это добавить
|
async def _subscribe(cls, topic_list: list[str], connection: WebSocket):
|
||||||
_ = pyg2o_main
|
async with cls._topic_lock:
|
||||||
_ = pyg2o_client
|
for topic in topic_list:
|
||||||
|
if topic not in cls._topics:
|
||||||
|
cls._topics[topic] = WeakSet()
|
||||||
|
cls._topics[topic].add(connection)
|
||||||
|
|
||||||
async def _handle_server_connection(self, websocket: WebSocket):
|
@classmethod
|
||||||
headers = websocket.headers
|
async def _unsubscribe(cls, topic_list: list[str], connection: WebSocket):
|
||||||
password = headers.get('Authorization')
|
async with cls._topic_lock:
|
||||||
|
for topic in topic_list:
|
||||||
if password != self._server_password:
|
cls._topics[topic].discard(connection)
|
||||||
# Закрытие до принятия подключения выбрасывает 403 (Forbidden) код, так что не нужны доп сообщения
|
|
||||||
await websocket.close()
|
@classmethod
|
||||||
|
async def _handle_connection(cls, connection: WebSocket):
|
||||||
|
|
||||||
|
if not await cls._process_headers(connection):
|
||||||
|
await connection.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._server_connection is not None:
|
await connection.accept()
|
||||||
await self._server_connection.close()
|
cls._logger.info('WebSocket клиент подключился')
|
||||||
|
|
||||||
await websocket.accept()
|
|
||||||
self._server_connection = websocket
|
|
||||||
self._logger.info('PyG2O сервер подключился')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
data = await websocket.receive_text()
|
data = await connection.receive_text()
|
||||||
message_data = json.loads(data)
|
message_data = json.loads(data)
|
||||||
self._logger.info(f'Сообщение сервера: {message_data}')
|
asyncio.create_task(cls._process_message(connection, message_data))
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
self._logger.exception(f'Ошибка декодирования JSON: {e}')
|
cls._logger.exception(f'Ошибка декодирования JSON: {e}')
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
self._logger.info('PyG2O сервер отключился')
|
cls._logger.info('WebSocket клиент отключился')
|
||||||
except WebSocketException as e:
|
except WebSocketException as e:
|
||||||
self._logger.exception(f'Ошибка подключения PyG2O сервера: {e}')
|
cls._logger.exception(f'Ошибка WebSocket подключения: {e}')
|
||||||
|
|
||||||
async def _process_server_message(self, message: dict):
|
@classmethod
|
||||||
|
async def _process_headers(cls, connection: WebSocket) -> bool:
|
||||||
|
headers = connection.headers
|
||||||
|
token = headers.get('Authorization')
|
||||||
|
topics = headers.get('Subscribe')
|
||||||
|
|
||||||
|
if token not in cls._static_tokens and token not in cls._temp_tokens:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if topics is not None:
|
||||||
|
topic_list = [s.strip() for s in topics.split(',')]
|
||||||
|
await cls._subscribe(topic_list, connection)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _process_message(cls, connection: WebSocket, message: dict):
|
||||||
match message:
|
match message:
|
||||||
case {'uuid': id, 'data': data}:
|
|
||||||
...
|
|
||||||
case {'data': data}:
|
|
||||||
...
|
|
||||||
case _:
|
|
||||||
raise ValueError(f'Неподдерживаемый тип PyG2O Server сообщения: {message}')
|
|
||||||
|
|
||||||
async def _handle_client_connection(self, websocket: WebSocket, playerid: int):
|
case {'uuid': id, 'data': data}:
|
||||||
...
|
if id in cls._requests:
|
||||||
|
cls._requests[id].set_result(data)
|
||||||
|
else:
|
||||||
|
asyncio.create_task(call_event('onWebsocketMessage', connection, id, data))
|
||||||
|
|
||||||
|
case {'event': event, **args}:
|
||||||
|
asyncio.create_task(call_event(event, **args))
|
||||||
|
|
||||||
|
case {'subscribe': topics}:
|
||||||
|
await cls._subscribe(topics, connection)
|
||||||
|
|
||||||
|
case {'unsubscribe': topics}:
|
||||||
|
await cls._unsubscribe(topics, connection)
|
||||||
|
|
||||||
|
case {'create_temp_token': token}:
|
||||||
|
cls._temp_tokens.append(token)
|
||||||
|
|
||||||
|
case {'remove_temp_token': token}:
|
||||||
|
cls._temp_tokens.remove(token)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f'Неподдерживаемый тип PyG2O сообщения: {message}')
|
||||||
|
|||||||
Reference in New Issue
Block a user