From c4a34c66309ca9130e99d03d4067c1ded408aa27 Mon Sep 17 00:00:00 2001 From: santaspeen Date: Mon, 15 Jul 2024 16:32:22 +0300 Subject: [PATCH] [~] Minor [>] main files > init.py [+] RateLimiter [~] Update BeamMP_version --- docs/cn/setup/readme.md | 4 +- docs/en/setup/readme.md | 4 +- docs/ru/setup/readme.md | 4 +- src/core/Client.py | 5 +- src/core/Client.pyi | 4 +- src/core/__init__.py | 5 +- src/core/core.py | 2 +- src/core/core.pyi | 2 +- src/core/tcp_server.py | 19 +- src/core/tcp_server.pyi | 3 + src/modules/ConfigProvider/__init__.py | 4 +- src/modules/ConsoleSystem/__init__.py | 268 ++++++++++++++++- src/modules/ConsoleSystem/console_system.py | 271 ------------------ ...{console_system-builtins.pyi => readme.md} | 15 +- src/modules/EventsSystem/__init__.py | 164 ++++++++++- src/modules/EventsSystem/events_system.py | 163 ----------- ...{events_systems-builtins.pyi => readme.md} | 5 +- src/modules/PluginsLoader/__init__.py | 220 +++++++++++++- .../PluginsLoader/lua_plugins_loader.py | 2 +- src/modules/PluginsLoader/plugins_loader.py | 219 -------------- src/modules/RateLimiter/__init__.py | 80 ++++++ src/modules/__init__.py | 1 + src/modules/i18n/__init__.py | 149 +++++++++- src/modules/i18n/i18n.py | 152 ---------- .../i18n/{i18n-builtins.pyi => readme.md} | 5 + 25 files changed, 921 insertions(+), 849 deletions(-) delete mode 100644 src/modules/ConsoleSystem/console_system.py rename src/modules/ConsoleSystem/{console_system-builtins.pyi => readme.md} (73%) delete mode 100644 src/modules/EventsSystem/events_system.py rename src/modules/EventsSystem/{events_systems-builtins.pyi => readme.md} (94%) delete mode 100644 src/modules/PluginsLoader/plugins_loader.py create mode 100644 src/modules/RateLimiter/__init__.py delete mode 100644 src/modules/i18n/i18n.py rename src/modules/i18n/{i18n-builtins.pyi => readme.md} (98%) diff --git a/docs/cn/setup/readme.md b/docs/cn/setup/readme.md index 4fa3988..2d9bcec 100644 --- a/docs/cn/setup/readme.md +++ b/docs/cn/setup/readme.md @@ -34,7 +34,7 @@ Auth: private: true Game: map: gridmap_v2 - max_cars: 1 + cars: 1 players: 8 Options: debug: false @@ -64,7 +64,7 @@ WebAPI: ### Game * `map` 仅为地图名称,即打开具有地图的 mod 在 `map.zip/levels` - 地图名称将在那里,那就是我们插入的地方。 -* `max_cars` - 每个玩家的最大汽车数量 +* `cars` - 每个玩家的最大汽车数量 * `players` - 最大玩家数 ### Options diff --git a/docs/en/setup/readme.md b/docs/en/setup/readme.md index 16b84a9..8cabb5b 100644 --- a/docs/en/setup/readme.md +++ b/docs/en/setup/readme.md @@ -34,7 +34,7 @@ Auth: private: true Game: map: gridmap_v2 - max_cars: 1 + cars: 1 players: 8 Options: debug: false @@ -64,7 +64,7 @@ WebAPI: ### Game * `map` is only the name of the map, i.e. open the mod with the map in `map.zip/levels` - the name of the map will be there, that's what we insert. -* `max_cars` - Maximum number of cars per player +* `cars` - Maximum number of cars per player * `players` - Maximum number of players ### Options diff --git a/docs/ru/setup/readme.md b/docs/ru/setup/readme.md index 8c9810f..33150d5 100644 --- a/docs/ru/setup/readme.md +++ b/docs/ru/setup/readme.md @@ -34,7 +34,7 @@ Auth: private: true Game: map: gridmap_v2 - max_cars: 1 + cars: 1 players: 8 Options: debug: false @@ -64,7 +64,7 @@ WebAPI: ### Game * `map` указывается только название карты, т.е. открываем мод с картой в `map.zip/levels` - вот тут будет название карты, его мы и вставляем -* `max_cars` - Максимальное количество машин на игрока +* `cars` - Максимальное количество машин на игрока * `players` - Максимально количество игроков ### Options diff --git a/src/core/Client.py b/src/core/Client.py index e5259ee..11e1803 100644 --- a/src/core/Client.py +++ b/src/core/Client.py @@ -9,6 +9,7 @@ import json import math import time import zlib +from asyncio import Lock from core import utils @@ -40,6 +41,7 @@ class Client: self._snowman = {"id": -1, "packet": ""} self._connect_time = 0 self._last_position = {} + self._lock = Lock() @property def _writer(self): @@ -193,7 +195,6 @@ class Client: writer.write(header + data) await writer.drain() return True - except Exception as e: self.log.debug(f'[TCP] Disconnected: {e}') self.__alive = False @@ -413,7 +414,7 @@ class Client: pass pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}" snowman = car_json.get("jbm") == "unicycle" - if allow and config.Game['max_cars'] > cars_count or (snowman and allow_snowman) or over_spawn: + if allow and config.Game['cars'] > cars_count or (snowman and allow_snowman) or over_spawn: if snowman: unicycle_id = self._snowman['id'] if unicycle_id != -1: diff --git a/src/core/Client.pyi b/src/core/Client.pyi index 0e1c6c0..b5cff24 100644 --- a/src/core/Client.pyi +++ b/src/core/Client.pyi @@ -5,7 +5,7 @@ # Licence: FPA # (c) kuitoi.su 2023 import asyncio -from asyncio import StreamReader, StreamWriter, DatagramTransport +from asyncio import StreamReader, StreamWriter, DatagramTransport, Lock from logging import Logger from typing import Tuple, List, Dict, Optional, Union, Any @@ -39,6 +39,8 @@ class Client: self._cars: List[Union[Dict[str, Union[str, bool, Dict[str, Union[str, List[int], float]]]], None]] = [] self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""} self._last_position = {} + self._lock = Lock() + async def __gracefully_kick(self): ... @property def _writer(self) -> StreamWriter: ... diff --git a/src/core/__init__.py b/src/core/__init__.py index 63e9104..05b0366 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -48,10 +48,9 @@ builtins.config = config config.enc = config.Options['encoding'] if config.Options['debug'] is True: utils.set_debug_status() - log.info("Debug enabled!") log = get_logger("core.init") - log.debug("Debug mode enabled!") - log.debug(f"Server config: {config}") + log.info("Debug mode enabled!") +log.debug(f"Server config: {config}") # i18n init log.debug("Initializing i18n...") ml = MultiLanguage() diff --git a/src/core/core.py b/src/core/core.py index b0d82f4..1ff0b64 100644 --- a/src/core/core.py +++ b/src/core/core.py @@ -47,7 +47,7 @@ class Core: self.lock_upload = False self.client_major_version = "2.0" - self.BeamMP_version = "3.1.1" # 20.07.2023 + self.BeamMP_version = "3.4.1" # 20.07.2023 ev.register("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")])) ev.register("_get_player", lambda x: self.get_client(**x['kwargs'])) diff --git a/src/core/core.pyi b/src/core/core.pyi index 512deae..049abde 100644 --- a/src/core/core.pyi +++ b/src/core/core.pyi @@ -36,7 +36,7 @@ class Core: self.web_stop: Callable = lambda: None self.lock_upload = False self.client_major_version = "2.0" - self.BeamMP_version = "3.2.0" + self.BeamMP_version = "3.4.1" def get_client(self, cid=None, nick=None) -> Client | None: ... async def insert_client(self, client: Client) -> None: ... def create_client(self, *args, **kwargs) -> Client: ... diff --git a/src/core/tcp_server.py b/src/core/tcp_server.py index 8038f8a..3d354eb 100644 --- a/src/core/tcp_server.py +++ b/src/core/tcp_server.py @@ -10,6 +10,7 @@ import traceback import aiohttp from core import utils +from modules import RateLimiter # noinspection PyProtectedMember @@ -21,6 +22,7 @@ class TCPServer: self.host = host self.port = port self.run = False + self.rl = RateLimiter(50, 10, 300) async def auth_client(self, reader, writer): client = self.Core.create_client(reader, writer) @@ -31,7 +33,8 @@ class TCPServer: await client.kick(i18n.core_player_kick_outdated) return False, client else: - await client._send(b"S") # Accepted client version + # await client._send(b"S") # Accepted client version + await client._send(b"A") # Accepted client version data = await client._recv(True) self.log.debug(f"Key: {data}") @@ -58,7 +61,8 @@ class TCPServer: # noinspection PyProtectedMember client._update_logger() except Exception as e: - self.log.error(f"Auth error: {e}") + self.log.error("Auth error.") + self.log.exception(e) await client.kick(i18n.core_player_kick_auth_server_fail) return False, client @@ -74,9 +78,9 @@ class TCPServer: lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers) for data in lua_data: if 1 == data: - allow = True + allow = False elif isinstance(data, str): - allow = True + allow = False reason = data if not allow: await client.kick(reason) @@ -103,7 +107,7 @@ class TCPServer: self.log.debug(f"Client: {client.nick}:{cid} - HandleDownload!") else: writer.close() - self.log.debug(f"Unknown client id:{cid} - HandleDownload") + self.log.debug(f"Unknown client :{cid} - HandleDownload") finally: return @@ -129,6 +133,10 @@ class TCPServer: async def handle_client(self, reader, writer): while True: try: + ip = writer.get_extra_info('peername')[0] + if self.rl.is_banned(ip): + self.rl.notify(ip, writer) + break data = await reader.read(1) if not data: break @@ -139,7 +147,6 @@ class TCPServer: _, cl = await self.handle_code(code, reader, writer) if cl: await cl._remove_me() - del cl break except Exception as e: self.log.error("Error while handling connection...") diff --git a/src/core/tcp_server.pyi b/src/core/tcp_server.pyi index 9ce347c..c223d34 100644 --- a/src/core/tcp_server.pyi +++ b/src/core/tcp_server.pyi @@ -10,6 +10,7 @@ from typing import Tuple from core import utils, Core from core.Client import Client +from modules import RateLimiter class TCPServer: @@ -20,6 +21,8 @@ class TCPServer: self.host = host self.port = port self.run = False + self.rl = RateLimiter(50, 10, 15) + async def auth_client(self, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ... async def set_down_rw(self, reader: StreamReader, writer: StreamWriter) -> bool: ... async def handle_code(self, code: str, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ... diff --git a/src/modules/ConfigProvider/__init__.py b/src/modules/ConfigProvider/__init__.py index 7c7303d..cdcc56a 100644 --- a/src/modules/ConfigProvider/__init__.py +++ b/src/modules/ConfigProvider/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Developed by KuiToi Dev -# File modules.config_provider.__init__.py +# File modules.ConfigProvider # Written by: SantaSpeen # Version 1.0 # Licence: FPA @@ -15,7 +15,7 @@ import yaml class Config: def __init__(self, auth=None, game=None, server=None, rcon=None, options=None, web=None): self.Auth = auth or {"key": None, "private": True} - self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1} + self.Game = game or {"map": "gridmap_v2", "players": 8, "cars": 1} self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!", "server_ip": "0.0.0.0", "server_port": 30814} self.RCON = rcon or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 10383, diff --git a/src/modules/ConsoleSystem/__init__.py b/src/modules/ConsoleSystem/__init__.py index c7f7939..56b375a 100644 --- a/src/modules/ConsoleSystem/__init__.py +++ b/src/modules/ConsoleSystem/__init__.py @@ -1,9 +1,271 @@ # -*- coding: utf-8 -*- # Developed by KuiToi Dev -# File modules.console.__init__.py +# File modules.ConsoleSystem # Written by: SantaSpeen -# Version 1.0 +# Version 1.2 # Licence: FPA # (c) kuitoi.su 2023 -from .console_system import Console +import builtins +import inspect +import logging +from typing import AnyStr + +from prompt_toolkit import PromptSession, print_formatted_text, HTML +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory +from prompt_toolkit.completion import NestedCompleter +from prompt_toolkit.history import FileHistory +try: + from prompt_toolkit.output.win32 import NoConsoleScreenBufferError +except AssertionError: + class NoConsoleScreenBufferError(Exception): ... +from prompt_toolkit.patch_stdout import patch_stdout + +from core import get_logger +from modules.ConsoleSystem.RCON import RCONSystem + + +class Console: + + def __init__(self, + prompt_in="> ", + prompt_out="", + not_found="Command \"%s\" not found in alias.", + debug=False) -> None: + self.__logger = get_logger("console") + self.__is_run = False + self.no_cmd = False + self.__prompt_in = prompt_in + self.__prompt_out = prompt_out + self.__not_found = not_found + self.__is_debug = debug + self.__print = print + self.__func = dict() + self.__alias = dict() + self.__man = dict() + self.__desc = dict() + self.__print_logger = get_logger("print") + self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man, + custom_completer={"man": {}}) + self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help, + custom_completer={"help": {"--raw": None}}) + self.completer = NestedCompleter.from_nested_dict(self.__alias) + rcon = RCONSystem + rcon.console = self + self.rcon = rcon + + def __debug(self, *x): + self.__logger.debug(f"{x}") + # if self.__is_debug: + # x = list(x) + # x.insert(0, "\r CONSOLE DEBUG:") + # self.__print(*x) + + def __getitem__(self, item): + print(item) + + @staticmethod + def __get_max_len(arg) -> int: + i = 0 + arg = list(arg) + for a in arg: + ln = len(str(a)) + if ln > i: + i = ln + return i + + def __create_man_message(self, argv: list) -> AnyStr: + if len(argv) == 0: + return self.__man.get("man") + x = argv[0] + if self.__alias.get(x) is None: + return i18n.man_command_not_found.format(x) + + man_message = self.__man.get(x) + if man_message: + return man_message + else: + return i18n.man_message_not_found + + # noinspection PyStringFormat + def __create_help_message(self, argv: list) -> AnyStr: + self.__debug("creating help message") + raw = False + max_len_v = 0 + if "--raw" in argv: + max_len_v = self.__get_max_len(self.__func.values()) + print() + raw = True + + message = "\n" + max_len = self.__get_max_len(self.__func.keys()) + if max_len < 7: + max_len = 7 + + if raw: + message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % ("Key", "Function", "Description") + else: + message += f" %-{max_len}s : %s\n" % (i18n.help_command, i18n.help_message) + + for k, v in self.__func.items(): + doc = self.__desc.get(k) + + if raw: + message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % (k, v, doc) + + else: + if doc is None: + doc = i18n.help_message_not_found + message += f" %-{max_len}s : %s\n" % (k, doc) + + return message + + def __update_completer(self): + self.completer = NestedCompleter.from_nested_dict(self.__alias) + + def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict: + key = key.format(" ", "-") + + if not isinstance(key, str): + raise TypeError("key must be string") + self.__debug(f"added user command: key={key}; func={func};") + self.__alias.update(custom_completer or {key: None}) + self.__alias["man"].update({key: None}) + self.__func.update({key: {"f": func}}) + self.__man.update({key: f'html:{i18n.man_for} {key}\n{man}' if man else None}) + self.__desc.update({key: desc}) + self.__update_completer() + return self.__alias.copy() + + def _write(self, t): + if self.no_cmd: + print(t) + return + try: + if t.startswith("html:"): + print_formatted_text(HTML(t[5:])) + else: + print_formatted_text(t) + except NoConsoleScreenBufferError: + print("Works in non cmd mode.") + self.no_cmd = True + print(t) + + def write(self, s: AnyStr): + if isinstance(s, (list, tuple)): + for text in s: + self._write(text) + else: + self._write(s) + + def log(self, s: AnyStr) -> None: + if isinstance(s, (list, tuple)): + for text in s: + self.__logger.info(f"{text}") + else: + self.__logger.info(f"{s}") + # self.write(s) + + def __lshift__(self, s: AnyStr) -> None: + self.write(s) + + @property + def alias(self) -> dict: + return self.__alias.copy() + + def __builtins_print(self, + *values: object, + sep: str or None = " ", + end: str or None = None, + file: str or None = None, + flush: bool = False) -> None: + self.__debug(f"Used __builtins_print; is_run: {self.__is_run}") + val = list(values) + if len(val) > 0: + if self.__is_run: + self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}") + else: + if end is None: + end = "\n" + self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush) + + def logger_hook(self) -> None: + self.__debug("used logger_hook") + + def emit(cls, record): + try: + msg = cls.format(record) + if cls.stream.name == "": + self.write(f"\r{msg}") + else: + cls.stream.write(msg + cls.terminator) + cls.flush() + except RecursionError: + raise + except Exception as e: + cls.handleError(record) + + logging.StreamHandler.emit = emit + + def builtins_hook(self) -> None: + self.__debug("used builtins_hook") + + builtins.Console = Console + builtins.console = self + + # builtins.print = self.__builtins_print + + async def read_input(self): + session = PromptSession(history=FileHistory('./.cmdhistory')) + while True: + try: + with patch_stdout(): + if self.no_cmd: + cmd_in = input(self.__prompt_in) + else: + try: + cmd_in = await session.prompt_async( + self.__prompt_in, + completer=self.completer, + auto_suggest=AutoSuggestFromHistory() + ) + except NoConsoleScreenBufferError: + print("Works in non cmd mode.") + self.no_cmd = True + cmd_s = cmd_in.split(" ") + cmd = cmd_s[0] + if cmd == "": + continue + else: + found_in_lua = False + d = ev.call_lua_event("onConsoleInput", cmd_in) + if len(d) > 0: + for text in d: + if text is not None: + found_in_lua = True + self.log(text) + command_object = self.__func.get(cmd) + if command_object: + func = command_object['f'] + if inspect.iscoroutinefunction(func): + out = await func(cmd_s[1:]) + else: + out = func(cmd_s[1:]) + if out: + self.log(out) + else: + if not found_in_lua: + self.log(self.__not_found % cmd) + except KeyboardInterrupt: + raise KeyboardInterrupt + except Exception as e: + print(f"Error in console.py: {e}") + self.__logger.exception(e) + + async def start(self): + self.__is_run = True + await self.read_input() + + def stop(self, *args, **kwargs): + self.__is_run = False + raise KeyboardInterrupt diff --git a/src/modules/ConsoleSystem/console_system.py b/src/modules/ConsoleSystem/console_system.py deleted file mode 100644 index 643a53b..0000000 --- a/src/modules/ConsoleSystem/console_system.py +++ /dev/null @@ -1,271 +0,0 @@ -# -*- coding: utf-8 -*- - -# Developed by KuiToi Dev -# File modules.ConsoleSystem.console_system.py -# Written by: SantaSpeen -# Version 1.2 -# Licence: FPA -# (c) kuitoi.su 2023 -import builtins -import inspect -import logging -from typing import AnyStr - -from prompt_toolkit import PromptSession, print_formatted_text, HTML -from prompt_toolkit.auto_suggest import AutoSuggestFromHistory -from prompt_toolkit.completion import NestedCompleter -from prompt_toolkit.history import FileHistory -try: - from prompt_toolkit.output.win32 import NoConsoleScreenBufferError -except AssertionError: - class NoConsoleScreenBufferError(Exception): ... -from prompt_toolkit.patch_stdout import patch_stdout - -from core import get_logger -from modules.ConsoleSystem.RCON import RCONSystem - - -class Console: - - def __init__(self, - prompt_in="> ", - prompt_out="", - not_found="Command \"%s\" not found in alias.", - debug=False) -> None: - self.__logger = get_logger("console") - self.__is_run = False - self.no_cmd = False - self.__prompt_in = prompt_in - self.__prompt_out = prompt_out - self.__not_found = not_found - self.__is_debug = debug - self.__print = print - self.__func = dict() - self.__alias = dict() - self.__man = dict() - self.__desc = dict() - self.__print_logger = get_logger("print") - self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man, - custom_completer={"man": {}}) - self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help, - custom_completer={"help": {"--raw": None}}) - self.completer = NestedCompleter.from_nested_dict(self.__alias) - rcon = RCONSystem - rcon.console = self - self.rcon = rcon - - def __debug(self, *x): - self.__logger.debug(f"{x}") - # if self.__is_debug: - # x = list(x) - # x.insert(0, "\r CONSOLE DEBUG:") - # self.__print(*x) - - def __getitem__(self, item): - print(item) - - @staticmethod - def __get_max_len(arg) -> int: - i = 0 - arg = list(arg) - for a in arg: - ln = len(str(a)) - if ln > i: - i = ln - return i - - def __create_man_message(self, argv: list) -> AnyStr: - if len(argv) == 0: - return self.__man.get("man") - x = argv[0] - if self.__alias.get(x) is None: - return i18n.man_command_not_found.format(x) - - man_message = self.__man.get(x) - if man_message: - return man_message - else: - return i18n.man_message_not_found - - # noinspection PyStringFormat - def __create_help_message(self, argv: list) -> AnyStr: - self.__debug("creating help message") - raw = False - max_len_v = 0 - if "--raw" in argv: - max_len_v = self.__get_max_len(self.__func.values()) - print() - raw = True - - message = "\n" - max_len = self.__get_max_len(self.__func.keys()) - if max_len < 7: - max_len = 7 - - if raw: - message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % ("Key", "Function", "Description") - else: - message += f" %-{max_len}s : %s\n" % (i18n.help_command, i18n.help_message) - - for k, v in self.__func.items(): - doc = self.__desc.get(k) - - if raw: - message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % (k, v, doc) - - else: - if doc is None: - doc = i18n.help_message_not_found - message += f" %-{max_len}s : %s\n" % (k, doc) - - return message - - def __update_completer(self): - self.completer = NestedCompleter.from_nested_dict(self.__alias) - - def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict: - key = key.format(" ", "-") - - if not isinstance(key, str): - raise TypeError("key must be string") - self.__debug(f"added user command: key={key}; func={func};") - self.__alias.update(custom_completer or {key: None}) - self.__alias["man"].update({key: None}) - self.__func.update({key: {"f": func}}) - self.__man.update({key: f'html:{i18n.man_for} {key}\n{man}' if man else None}) - self.__desc.update({key: desc}) - self.__update_completer() - return self.__alias.copy() - - def _write(self, t): - if self.no_cmd: - print(t) - return - try: - if t.startswith("html:"): - print_formatted_text(HTML(t[5:])) - else: - print_formatted_text(t) - except NoConsoleScreenBufferError: - print("Works in non cmd mode.") - self.no_cmd = True - print(t) - - def write(self, s: AnyStr): - if isinstance(s, (list, tuple)): - for text in s: - self._write(text) - else: - self._write(s) - - def log(self, s: AnyStr) -> None: - if isinstance(s, (list, tuple)): - for text in s: - self.__logger.info(f"{text}") - else: - self.__logger.info(f"{s}") - # self.write(s) - - def __lshift__(self, s: AnyStr) -> None: - self.write(s) - - @property - def alias(self) -> dict: - return self.__alias.copy() - - def __builtins_print(self, - *values: object, - sep: str or None = " ", - end: str or None = None, - file: str or None = None, - flush: bool = False) -> None: - self.__debug(f"Used __builtins_print; is_run: {self.__is_run}") - val = list(values) - if len(val) > 0: - if self.__is_run: - self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}") - else: - if end is None: - end = "\n" - self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush) - - def logger_hook(self) -> None: - self.__debug("used logger_hook") - - def emit(cls, record): - try: - msg = cls.format(record) - if cls.stream.name == "": - self.write(f"\r{msg}") - else: - cls.stream.write(msg + cls.terminator) - cls.flush() - except RecursionError: - raise - except Exception as e: - cls.handleError(record) - - logging.StreamHandler.emit = emit - - def builtins_hook(self) -> None: - self.__debug("used builtins_hook") - - builtins.Console = Console - builtins.console = self - - # builtins.print = self.__builtins_print - - async def read_input(self): - session = PromptSession(history=FileHistory('./.cmdhistory')) - while True: - try: - with patch_stdout(): - if self.no_cmd: - cmd_in = input(self.__prompt_in) - else: - try: - cmd_in = await session.prompt_async( - self.__prompt_in, - completer=self.completer, - auto_suggest=AutoSuggestFromHistory() - ) - except NoConsoleScreenBufferError: - print("Works in non cmd mode.") - self.no_cmd = True - cmd_s = cmd_in.split(" ") - cmd = cmd_s[0] - if cmd == "": - continue - else: - found_in_lua = False - d = ev.call_lua_event("onConsoleInput", cmd_in) - if len(d) > 0: - for text in d: - if text is not None: - found_in_lua = True - self.log(text) - command_object = self.__func.get(cmd) - if command_object: - func = command_object['f'] - if inspect.iscoroutinefunction(func): - out = await func(cmd_s[1:]) - else: - out = func(cmd_s[1:]) - if out: - self.log(out) - else: - if not found_in_lua: - self.log(self.__not_found % cmd) - except KeyboardInterrupt: - raise KeyboardInterrupt - except Exception as e: - print(f"Error in console.py: {e}") - self.__logger.exception(e) - - async def start(self): - self.__is_run = True - await self.read_input() - - def stop(self, *args, **kwargs): - self.__is_run = False - raise KeyboardInterrupt diff --git a/src/modules/ConsoleSystem/console_system-builtins.pyi b/src/modules/ConsoleSystem/readme.md similarity index 73% rename from src/modules/ConsoleSystem/console_system-builtins.pyi rename to src/modules/ConsoleSystem/readme.md index 6238aea..124eea4 100644 --- a/src/modules/ConsoleSystem/console_system-builtins.pyi +++ b/src/modules/ConsoleSystem/readme.md @@ -1,18 +1,10 @@ -from logging import Logger -from typing import AnyStr - -from core import get_logger - +### Builtins +```python class RCONSystem: console = None - def __init__(self, key, host, port): - self.log = get_logger("RCON") - self.key = key - self.host = host - self.port = port - + def __init__(self, key, host, port): ... async def start(self): ... async def stop(self): ... @@ -35,3 +27,4 @@ class console: def write(s: str) -> None: ... @staticmethod def __lshift__(s: AnyStr) -> None: ... +``` diff --git a/src/modules/EventsSystem/__init__.py b/src/modules/EventsSystem/__init__.py index b460c84..2cd835b 100644 --- a/src/modules/EventsSystem/__init__.py +++ b/src/modules/EventsSystem/__init__.py @@ -1 +1,163 @@ -from .events_system import EventsSystem +# -*- coding: utf-8 -*- + +# Developed by KuiToi Dev +# File modules.EventsSystem +# Written by: SantaSpeen +# Version 1.0 +# Licence: FPA +# (c) kuitoi.su 2023 +import asyncio +import builtins +import inspect + +from core import get_logger + + +# noinspection PyShadowingBuiltins +class EventsSystem: + + def __init__(self): + # TODO: default events + self.log = get_logger("EventsSystem") + self.loop = asyncio.get_event_loop() + self.as_tasks = [] + self.__events = { + "onServerStarted": [], # No handler + "onPlayerSentKey": [], # Only sync, no handler + "onPlayerAuthenticated": [], # (!) Only sync, With handler + "onPlayerJoin": [], # (!) With handler + "onChatReceive": [], # (!) With handler + "onCarSpawn": [], # (!) With handler + "onCarDelete": [], # (!) With handler (admin allow) + "onCarEdited": [], # (!) With handler + "onCarReset": [], # No handler + "onCarChanged": [], # No handler + "onCarFocusMove": [], # No handler + "onSentPing": [], # Only sync, no handler + "onChangePosition": [], # Only sync, no handler + "onPlayerDisconnect": [], # No handler + "onServerStopped": [], # No handler + } + self.__async_events = { + "onServerStarted": [], + "onPlayerJoin": [], + "onChatReceive": [], + "onCarSpawn": [], + "onCarDelete": [], + "onCarEdited": [], + "onCarReset": [], + "onCarChanged": [], + "onCarFocusMove": [], + "onPlayerDisconnect": [], + "onServerStopped": [] + } + + self.__lua_events = { + "onInit": [], # onServerStarted + "onShutdown": [], # onServerStopped + "onPlayerAuth": [], # onPlayerAuthenticated + "onPlayerConnecting": [], # No + "onPlayerJoining": [], # No + "onPlayerJoin": [], # onPlayerJoin + "onPlayerDisconnect": [], # onPlayerDisconnect + "onChatMessage": [], # onChatReceive + "onVehicleSpawn": [], # onCarSpawn + "onVehicleEdited": [], # onCarEdited + "onVehicleDeleted": [], # onCarDelete + "onVehicleReset": [], # onCarReset + "onFileChanged": [], # TODO lua onFileChanged + "onConsoleInput": [], # kt.add_command + } + self.register_event = self.register + + def builtins_hook(self): + self.log.debug("used builtins_hook") + builtins.ev = self + + def is_event(self, event_name): + return (event_name in self.__async_events.keys() or + event_name in self.__events.keys() or + event_name in self.__lua_events.keys()) + + def register(self, event_name, event_func, async_event=False, lua=None): + self.log.debug(f"register(event_name='{event_name}', event_func='{event_func}', " + f"async_event={async_event}, lua_event={lua}):") + if lua: + if event_name not in self.__lua_events: + self.__lua_events.update({str(event_name): [{"func_name": event_func, "lua": lua}]}) + else: + self.__lua_events[event_name].append({"func_name": event_func, "lua": lua}) + self.log.debug("Register ok") + return + + if not callable(event_func): + self.log.error(i18n.events_not_callable.format(event_name, f"kt.add_event(\"{event_name}\", function)")) + return + if async_event or inspect.iscoroutinefunction(event_func): + if event_name not in self.__async_events: + self.__async_events.update({str(event_name): [event_func]}) + else: + self.__async_events[event_name].append(event_func) + self.log.debug("Register ok") + else: + if event_name not in self.__events: + self.__events.update({str(event_name): [event_func]}) + else: + self.__events[event_name].append(event_func) + self.log.debug("Register ok") + + async def call_async_event(self, event_name, *args, **kwargs): + self.log.debug(f"Calling async event: '{event_name}'") + funcs_data = [] + if event_name in self.__async_events.keys(): + for func in self.__async_events[event_name]: + try: + event_data = {"event_name": event_name, "args": args, "kwargs": kwargs} + data = await func(event_data) + funcs_data.append(data) + except Exception as e: + self.log.error(i18n.events_calling_error.format(event_name, func.__name__)) + self.log.exception(e) + elif not self.is_event(event_name): + self.log.warning(i18n.events_not_found.format(event_name, "kt.call_event()")) + + return funcs_data + + def call_event(self, event_name, *args, **kwargs): + if event_name not in ["onChangePosition", "onSentPing"]: # UDP events + self.log.debug(f"Calling sync event: '{event_name}'") + funcs_data = [] + + if event_name in self.__events.keys(): + for func in self.__events[event_name]: + try: + event_data = {"event_name": event_name, "args": args, "kwargs": kwargs} + funcs_data.append(func(event_data)) + except Exception as e: + self.log.error(i18n.events_calling_error.format(event_name, func.__name__)) + self.log.exception(e) + elif not self.is_event(event_name): + self.log.warning(i18n.events_not_found.format(event_name, "kt.call_async_event()")) + + return funcs_data + + def call_lua_event(self, event_name, *args): + self.log.debug(f"Calling lua event: '{event_name}{args}'") + funcs_data = [] + if event_name in self.__lua_events.keys(): + for data in self.__lua_events[event_name]: + lua = data['lua'] + func_name = data["func_name"] + try: + func = lua.globals()[func_name] + if not func: + self.log.warning(i18n.events_lua_function_not_found.format("", func_name)) + continue + fd = func(*args) + funcs_data.append(fd) + except Exception as e: + self.log.error(i18n.events_lua_calling_error.format(f"{e}", event_name, func_name, f"{args}")) + elif not self.is_event(event_name): + self.log.warning(i18n.events_not_found.format(event_name, "ev.call_lua_event(), MP.Trigger<>Event()")) + + return funcs_data diff --git a/src/modules/EventsSystem/events_system.py b/src/modules/EventsSystem/events_system.py deleted file mode 100644 index 67f4d68..0000000 --- a/src/modules/EventsSystem/events_system.py +++ /dev/null @@ -1,163 +0,0 @@ -# -*- coding: utf-8 -*- - -# Developed by KuiToi Dev -# File modules.EventsSystem.events_system.py -# Written by: SantaSpeen -# Version 1.0 -# Licence: FPA -# (c) kuitoi.su 2023 -import asyncio -import builtins -import inspect - -from core import get_logger - - -# noinspection PyShadowingBuiltins -class EventsSystem: - - def __init__(self): - # TODO: default events - self.log = get_logger("EventsSystem") - self.loop = asyncio.get_event_loop() - self.as_tasks = [] - self.__events = { - "onServerStarted": [], # No handler - "onPlayerSentKey": [], # Only sync, no handler - "onPlayerAuthenticated": [], # (!) Only sync, With handler - "onPlayerJoin": [], # (!) With handler - "onChatReceive": [], # (!) With handler - "onCarSpawn": [], # (!) With handler - "onCarDelete": [], # (!) With handler (admin allow) - "onCarEdited": [], # (!) With handler - "onCarReset": [], # No handler - "onCarChanged": [], # No handler - "onCarFocusMove": [], # No handler - "onSentPing": [], # Only sync, no handler - "onChangePosition": [], # Only sync, no handler - "onPlayerDisconnect": [], # No handler - "onServerStopped": [], # No handler - } - self.__async_events = { - "onServerStarted": [], - "onPlayerJoin": [], - "onChatReceive": [], - "onCarSpawn": [], - "onCarDelete": [], - "onCarEdited": [], - "onCarReset": [], - "onCarChanged": [], - "onCarFocusMove": [], - "onPlayerDisconnect": [], - "onServerStopped": [] - } - - self.__lua_events = { - "onInit": [], # onServerStarted - "onShutdown": [], # onServerStopped - "onPlayerAuth": [], # onPlayerAuthenticated - "onPlayerConnecting": [], # No - "onPlayerJoining": [], # No - "onPlayerJoin": [], # onPlayerJoin - "onPlayerDisconnect": [], # onPlayerDisconnect - "onChatMessage": [], # onChatReceive - "onVehicleSpawn": [], # onCarSpawn - "onVehicleEdited": [], # onCarEdited - "onVehicleDeleted": [], # onCarDelete - "onVehicleReset": [], # onCarReset - "onFileChanged": [], # TODO lua onFileChanged - "onConsoleInput": [], # kt.add_command - } - self.register_event = self.register - - def builtins_hook(self): - self.log.debug("used builtins_hook") - builtins.ev = self - - def is_event(self, event_name): - return (event_name in self.__async_events.keys() or - event_name in self.__events.keys() or - event_name in self.__lua_events.keys()) - - def register(self, event_name, event_func, async_event=False, lua=None): - self.log.debug(f"register(event_name='{event_name}', event_func='{event_func}', " - f"async_event={async_event}, lua_event={lua}):") - if lua: - if event_name not in self.__lua_events: - self.__lua_events.update({str(event_name): [{"func_name": event_func, "lua": lua}]}) - else: - self.__lua_events[event_name].append({"func_name": event_func, "lua": lua}) - self.log.debug("Register ok") - return - - if not callable(event_func): - self.log.error(i18n.events_not_callable.format(event_name, f"kt.add_event(\"{event_name}\", function)")) - return - if async_event or inspect.iscoroutinefunction(event_func): - if event_name not in self.__async_events: - self.__async_events.update({str(event_name): [event_func]}) - else: - self.__async_events[event_name].append(event_func) - self.log.debug("Register ok") - else: - if event_name not in self.__events: - self.__events.update({str(event_name): [event_func]}) - else: - self.__events[event_name].append(event_func) - self.log.debug("Register ok") - - async def call_async_event(self, event_name, *args, **kwargs): - self.log.debug(f"Calling async event: '{event_name}'") - funcs_data = [] - if event_name in self.__async_events.keys(): - for func in self.__async_events[event_name]: - try: - event_data = {"event_name": event_name, "args": args, "kwargs": kwargs} - data = await func(event_data) - funcs_data.append(data) - except Exception as e: - self.log.error(i18n.events_calling_error.format(event_name, func.__name__)) - self.log.exception(e) - elif not self.is_event(event_name): - self.log.warning(i18n.events_not_found.format(event_name, "kt.call_event()")) - - return funcs_data - - def call_event(self, event_name, *args, **kwargs): - if event_name not in ["onChangePosition", "onSentPing"]: # UDP events - self.log.debug(f"Calling sync event: '{event_name}'") - funcs_data = [] - - if event_name in self.__events.keys(): - for func in self.__events[event_name]: - try: - event_data = {"event_name": event_name, "args": args, "kwargs": kwargs} - funcs_data.append(func(event_data)) - except Exception as e: - self.log.error(i18n.events_calling_error.format(event_name, func.__name__)) - self.log.exception(e) - elif not self.is_event(event_name): - self.log.warning(i18n.events_not_found.format(event_name, "kt.call_async_event()")) - - return funcs_data - - def call_lua_event(self, event_name, *args): - self.log.debug(f"Calling lua event: '{event_name}{args}'") - funcs_data = [] - if event_name in self.__lua_events.keys(): - for data in self.__lua_events[event_name]: - lua = data['lua'] - func_name = data["func_name"] - try: - func = lua.globals()[func_name] - if not func: - self.log.warning(i18n.events_lua_function_not_found.format("", func_name)) - continue - fd = func(*args) - funcs_data.append(fd) - except Exception as e: - self.log.error(i18n.events_lua_calling_error.format(f"{e}", event_name, func_name, f"{args}")) - elif not self.is_event(event_name): - self.log.warning(i18n.events_not_found.format(event_name, "ev.call_lua_event(), MP.Trigger<>Event()")) - - return funcs_data diff --git a/src/modules/EventsSystem/events_systems-builtins.pyi b/src/modules/EventsSystem/readme.md similarity index 94% rename from src/modules/EventsSystem/events_systems-builtins.pyi rename to src/modules/EventsSystem/readme.md index 6616288..00a88bf 100644 --- a/src/modules/EventsSystem/events_systems-builtins.pyi +++ b/src/modules/EventsSystem/readme.md @@ -1,6 +1,6 @@ -from typing import Any - +### Builtins +```python class EventsSystem: @staticmethod def register(event_name, event_func, async_event: bool = False, lua: bool | object = None): ... @@ -11,3 +11,4 @@ class EventsSystem: @staticmethod def call_lua_event(event_name, *data) -> list[Any]: ... class ev(EventsSystem): ... +``` \ No newline at end of file diff --git a/src/modules/PluginsLoader/__init__.py b/src/modules/PluginsLoader/__init__.py index f1cb068..2a0fcd4 100644 --- a/src/modules/PluginsLoader/__init__.py +++ b/src/modules/PluginsLoader/__init__.py @@ -1 +1,219 @@ -from .plugins_loader import PluginsLoader +# -*- coding: utf-8 -*- + +# Developed by KuiToi Dev +# File modules.PluginsLoader +# Written by: SantaSpeen +# Version 1.0 +# Licence: FPA +# (c) kuitoi.su 2023 +import asyncio +import inspect +import os +import types +from contextlib import contextmanager +from threading import Thread + +from core import get_logger + + +class KuiToi: + _plugins_dir = "" + + def __init__(self, name): + if name is None: + raise AttributeError("KuiToi: Name is required") + self.__log = get_logger(f"Plugin | {name}") + self.__name = name + self.__dir = os.path.join(self._plugins_dir, self.__name) + if not os.path.exists(self.__dir): + os.mkdir(self.__dir) + + @property + def log(self): + return self.__log + + @property + def name(self): + return self.__name + + @property + def dir(self): + return self.__dir + + @contextmanager + def open(self, file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None): + path = os.path.join(self.__dir, file) + self.log.debug(f'Trying to open "{path}" with mode "{mode}"') + # Really need? + # if not os.path.exists(path): + # with open(path, 'x'): ... + f = None + try: + f = open(path, mode, buffering, encoding, errors, newline, closefd, opener) + yield f + except Exception as e: + raise e + finally: + if f is not None: + f.close() + + def register(self, event_name, event_func): + self.log.debug(f"Registering event {event_name}") + ev.register(event_name, event_func) + + def call_event(self, event_name, *args, **kwargs): + self.log.debug(f"Called event {event_name}") + return ev.call_event(event_name, *args, **kwargs) + + async def call_async_event(self, event_name, *args, **kwargs): + self.log.debug(f"Called async event {event_name}") + return await ev.call_async_event(event_name, *args, **kwargs) + + def call_lua_event(self, event_name, *args): + self.log.debug(f"Called lua event {event_name}") + return ev.call_lua_event(event_name, *args) + + def get_player(self, pid=None, nick=None, cid=None): + self.log.debug("Requests get_player") + return ev.call_event("_get_player", cid=cid or pid, nick=nick)[0] + + def get_players(self): + self.log.debug("Requests get_players") + return self.get_player(-1) + + def players_counter(self): + self.log.debug("Requests players_counter") + return len(self.get_players()) + + def is_player_connected(self, pid=None, nick=None): + self.log.debug("Requests is_player_connected") + if pid < 0: + return False + return bool(self.get_player(cid=pid, nick=nick)) + + def add_command(self, key, func, man, desc, custom_completer) -> dict: + self.log.debug("Requests add_command") + return console.add_command(key, func, man, desc, custom_completer) + + +class PluginsLoader: + + def __init__(self, plugins_dir): + self.loop = asyncio.get_event_loop() + self.plugins = {} + self.plugins_tasks = [] + self.plugins_dir = plugins_dir + self.log = get_logger("PluginsLoader") + self.loaded_str = "Plugins: " + ev.register("_plugins_start", self.start) + ev.register("_plugins_unload", self.unload) + ev.register("_plugins_get", lambda x: list(self.plugins.keys())) + console.add_command("plugins", lambda x: self.loaded_str[:-2]) + console.add_command("pl", lambda x: self.loaded_str[:-2]) + + async def load(self): + self.log.debug("Loading plugins...") + for file in os.listdir(self.plugins_dir): + file_path = os.path.join(self.plugins_dir, file) + if os.path.isfile(file_path) and file.endswith(".py"): + try: + self.log.debug(f"Loading plugin: {file[:-3]}") + plugin = types.ModuleType(file[:-3]) + plugin.KuiToi = KuiToi + plugin.KuiToi._plugins_dir = self.plugins_dir + plugin.print = print + plugin.__file__ = file_path + with open(f'{file_path}', 'r', encoding=config.enc) as f: + code = f.read() + exec(code, plugin.__dict__) + + ok = True + try: + is_func = inspect.isfunction + if not is_func(plugin.load): + self.log.error(i18n.plugins_not_found_load) + ok = False + if not is_func(plugin.start): + self.log.error(i18n.plugins_not_found_start) + ok = False + if not is_func(plugin.unload): + self.log.error(i18n.plugins_not_found_unload) + ok = False + if type(plugin.kt) != KuiToi: + self.log.error(i18n.plugins_kt_invalid) + ok = False + except AttributeError: + ok = False + if not ok: + self.log.error(i18n.plugins_invalid.format(file_path)) + return + + pl_name = plugin.kt.name + if self.plugins.get(pl_name) is not None: + raise NameError(f'Having plugins with identical names is not allowed; ' + f'Plugin name: "{pl_name}"; Plugin file "{file_path}"') + + plugin.open = plugin.kt.open + is_coro_func = inspect.iscoroutinefunction + self.plugins.update( + { + pl_name: { + "plugin": plugin, + "load": { + "func": plugin.load, + "async": is_coro_func(plugin.load) + }, + "start": { + "func": plugin.start, + "async": is_coro_func(plugin.start) + }, + "unload": { + "func": plugin.unload, + "async": is_coro_func(plugin.unload) + } + } + } + ) + if self.plugins[pl_name]["load"]['async']: + plugin.log.debug(f"I'm async") + await plugin.load() + else: + plugin.log.debug(f"I'm sync") + th = Thread(target=plugin.load, name=f"{pl_name}.load()") + th.start() + th.join() + self.loaded_str += f"{pl_name}:ok, " + self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}") + except Exception as e: + self.loaded_str += f"{file}:no, " + self.log.error(i18n.plugins_error_loading.format(file, f"{e}")) + self.log.exception(e) + + async def start(self, _): + for pl_name, pl_data in self.plugins.items(): + try: + if pl_data['start']['async']: + self.log.debug(f"Start async plugin: {pl_name}") + t = self.loop.create_task(pl_data['start']['func']()) + self.plugins_tasks.append(t) + else: + self.log.debug(f"Start sync plugin: {pl_name}") + th = Thread(target=pl_data['start']['func'], name=f"Thread {pl_name}") + th.start() + self.plugins_tasks.append(th) + except Exception as e: + self.log.exception(e) + + async def unload(self, _): + for pl_name, pl_data in self.plugins.items(): + try: + if pl_data['unload']['async']: + self.log.debug(f"Unload async plugin: {pl_name}") + await pl_data['unload']['func']() + else: + self.log.debug(f"Unload sync plugin: {pl_name}") + th = Thread(target=pl_data['unload']['func'], name=f"Thread {pl_name}") + th.start() + th.join() + except Exception as e: + self.log.exception(e) diff --git a/src/modules/PluginsLoader/lua_plugins_loader.py b/src/modules/PluginsLoader/lua_plugins_loader.py index d5261db..4508f22 100644 --- a/src/modules/PluginsLoader/lua_plugins_loader.py +++ b/src/modules/PluginsLoader/lua_plugins_loader.py @@ -598,7 +598,7 @@ class LuaPluginsLoader: "LogChat": config.Options['log_chat'], "Debug": config.Options['debug'], "Private": config.Auth['private'], - "MaxCars": config.Game['max_cars'], + "MaxCars": config.Game['cars'], "MaxPlayers": config.Game['players'], "Map": f"/levels/{config.Game['map']}/info.json", "Description": config.Server['description'], diff --git a/src/modules/PluginsLoader/plugins_loader.py b/src/modules/PluginsLoader/plugins_loader.py deleted file mode 100644 index da295e2..0000000 --- a/src/modules/PluginsLoader/plugins_loader.py +++ /dev/null @@ -1,219 +0,0 @@ -# -*- coding: utf-8 -*- - -# Developed by KuiToi Dev -# File modules.PluginsLoader.plugins_loader.py -# Written by: SantaSpeen -# Version 1.0 -# Licence: FPA -# (c) kuitoi.su 2023 -import asyncio -import inspect -import os -import types -from contextlib import contextmanager -from threading import Thread - -from core import get_logger - - -class KuiToi: - _plugins_dir = "" - - def __init__(self, name): - if name is None: - raise AttributeError("KuiToi: Name is required") - self.__log = get_logger(f"Plugin | {name}") - self.__name = name - self.__dir = os.path.join(self._plugins_dir, self.__name) - if not os.path.exists(self.__dir): - os.mkdir(self.__dir) - - @property - def log(self): - return self.__log - - @property - def name(self): - return self.__name - - @property - def dir(self): - return self.__dir - - @contextmanager - def open(self, file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None): - path = os.path.join(self.__dir, file) - self.log.debug(f'Trying to open "{path}" with mode "{mode}"') - # Really need? - # if not os.path.exists(path): - # with open(path, 'x'): ... - f = None - try: - f = open(path, mode, buffering, encoding, errors, newline, closefd, opener) - yield f - except Exception as e: - raise e - finally: - if f is not None: - f.close() - - def register(self, event_name, event_func): - self.log.debug(f"Registering event {event_name}") - ev.register(event_name, event_func) - - def call_event(self, event_name, *args, **kwargs): - self.log.debug(f"Called event {event_name}") - return ev.call_event(event_name, *args, **kwargs) - - async def call_async_event(self, event_name, *args, **kwargs): - self.log.debug(f"Called async event {event_name}") - return await ev.call_async_event(event_name, *args, **kwargs) - - def call_lua_event(self, event_name, *args): - self.log.debug(f"Called lua event {event_name}") - return ev.call_lua_event(event_name, *args) - - def get_player(self, pid=None, nick=None, cid=None): - self.log.debug("Requests get_player") - return ev.call_event("_get_player", cid=cid or pid, nick=nick)[0] - - def get_players(self): - self.log.debug("Requests get_players") - return self.get_player(-1) - - def players_counter(self): - self.log.debug("Requests players_counter") - return len(self.get_players()) - - def is_player_connected(self, pid=None, nick=None): - self.log.debug("Requests is_player_connected") - if pid < 0: - return False - return bool(self.get_player(cid=pid, nick=nick)) - - def add_command(self, key, func, man, desc, custom_completer) -> dict: - self.log.debug("Requests add_command") - return console.add_command(key, func, man, desc, custom_completer) - - -class PluginsLoader: - - def __init__(self, plugins_dir): - self.loop = asyncio.get_event_loop() - self.plugins = {} - self.plugins_tasks = [] - self.plugins_dir = plugins_dir - self.log = get_logger("PluginsLoader") - self.loaded_str = "Plugins: " - ev.register("_plugins_start", self.start) - ev.register("_plugins_unload", self.unload) - ev.register("_plugins_get", lambda x: list(self.plugins.keys())) - console.add_command("plugins", lambda x: self.loaded_str[:-2]) - console.add_command("pl", lambda x: self.loaded_str[:-2]) - - async def load(self): - self.log.debug("Loading plugins...") - for file in os.listdir(self.plugins_dir): - file_path = os.path.join(self.plugins_dir, file) - if os.path.isfile(file_path) and file.endswith(".py"): - try: - self.log.debug(f"Loading plugin: {file[:-3]}") - plugin = types.ModuleType(file[:-3]) - plugin.KuiToi = KuiToi - plugin.KuiToi._plugins_dir = self.plugins_dir - plugin.print = print - plugin.__file__ = file_path - with open(f'{file_path}', 'r', encoding=config.enc) as f: - code = f.read() - exec(code, plugin.__dict__) - - ok = True - try: - is_func = inspect.isfunction - if not is_func(plugin.load): - self.log.error(i18n.plugins_not_found_load) - ok = False - if not is_func(plugin.start): - self.log.error(i18n.plugins_not_found_start) - ok = False - if not is_func(plugin.unload): - self.log.error(i18n.plugins_not_found_unload) - ok = False - if type(plugin.kt) != KuiToi: - self.log.error(i18n.plugins_kt_invalid) - ok = False - except AttributeError: - ok = False - if not ok: - self.log.error(i18n.plugins_invalid.format(file_path)) - return - - pl_name = plugin.kt.name - if self.plugins.get(pl_name) is not None: - raise NameError(f'Having plugins with identical names is not allowed; ' - f'Plugin name: "{pl_name}"; Plugin file "{file_path}"') - - plugin.open = plugin.kt.open - is_coro_func = inspect.iscoroutinefunction - self.plugins.update( - { - pl_name: { - "plugin": plugin, - "load": { - "func": plugin.load, - "async": is_coro_func(plugin.load) - }, - "start": { - "func": plugin.start, - "async": is_coro_func(plugin.start) - }, - "unload": { - "func": plugin.unload, - "async": is_coro_func(plugin.unload) - } - } - } - ) - if self.plugins[pl_name]["load"]['async']: - plugin.log.debug(f"I'm async") - await plugin.load() - else: - plugin.log.debug(f"I'm sync") - th = Thread(target=plugin.load, name=f"{pl_name}.load()") - th.start() - th.join() - self.loaded_str += f"{pl_name}:ok, " - self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}") - except Exception as e: - self.loaded_str += f"{file}:no, " - self.log.error(i18n.plugins_error_loading.format(file, f"{e}")) - self.log.exception(e) - - async def start(self, _): - for pl_name, pl_data in self.plugins.items(): - try: - if pl_data['start']['async']: - self.log.debug(f"Start async plugin: {pl_name}") - t = self.loop.create_task(pl_data['start']['func']()) - self.plugins_tasks.append(t) - else: - self.log.debug(f"Start sync plugin: {pl_name}") - th = Thread(target=pl_data['start']['func'], name=f"Thread {pl_name}") - th.start() - self.plugins_tasks.append(th) - except Exception as e: - self.log.exception(e) - - async def unload(self, _): - for pl_name, pl_data in self.plugins.items(): - try: - if pl_data['unload']['async']: - self.log.debug(f"Unload async plugin: {pl_name}") - await pl_data['unload']['func']() - else: - self.log.debug(f"Unload sync plugin: {pl_name}") - th = Thread(target=pl_data['unload']['func'], name=f"Thread {pl_name}") - th.start() - th.join() - except Exception as e: - self.log.exception(e) diff --git a/src/modules/RateLimiter/__init__.py b/src/modules/RateLimiter/__init__.py new file mode 100644 index 0000000..8ebc7b3 --- /dev/null +++ b/src/modules/RateLimiter/__init__.py @@ -0,0 +1,80 @@ +import asyncio +from collections import defaultdict, deque +from datetime import datetime, timedelta + +from core import utils + + +class RateLimiter: + def __init__(self, max_calls: int, period: float, ban_time: float): + self.log = utils.get_logger("DOSProtect") + self.max_calls = max_calls + self.period = timedelta(seconds=period) + self.ban_time = timedelta(seconds=ban_time) + self._calls = defaultdict(deque) + self._banned_until = defaultdict(lambda: datetime.min) + self._notified = {} + + async def notify(self, ip, writer): + if not self._notified[ip]: + self._notified[ip] = True + self.log.warning(f"{ip} banned until {self._banned_until[ip]}.") + try: + writer.write(b'\x0b\x00\x00\x00Eip banned.') + await writer.drain() + writer.close() + except Exception: + pass + + def is_banned(self, ip: str) -> bool: + now = datetime.now() + if now < self._banned_until[ip]: + return True + + now = datetime.now() + self._calls[ip].append(now) + + while self._calls[ip] and self._calls[ip][0] + self.period < now: + self._calls[ip].popleft() + + if len(self._calls[ip]) > self.max_calls: + self._banned_until[ip] = now + self.ban_time + self._calls[ip].clear() + return True + + self._notified[ip] = False + return False + + +async def handle_request(ip: str, rate_limiter: RateLimiter): + if rate_limiter.is_banned(ip): + print(f"Request from {ip} is banned at {datetime.now()}") + print(f"{rate_limiter._banned_until[ip]}") + return + + +async def server_simulation(): + rate_limiter = RateLimiter(max_calls=5, period=10, ban_time=30) + + # Симулируем несколько запросов от разных IP-адресов + tasks = [ + handle_request("192.168.1.1", rate_limiter), + handle_request("192.168.1.2", rate_limiter), + handle_request("192.168.1.1", rate_limiter), + handle_request("192.168.1.1", rate_limiter), + handle_request("192.168.1.3", rate_limiter), + handle_request("192.168.1.2", rate_limiter), + handle_request("192.168.1.1", rate_limiter), + handle_request("192.168.1.2", rate_limiter), + handle_request("192.168.1.3", rate_limiter), + handle_request("192.168.1.1", rate_limiter), + handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban + handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban + handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban + ] + + await asyncio.gather(*tasks) + + +if __name__ == "__main__": + asyncio.run(server_simulation()) diff --git a/src/modules/__init__.py b/src/modules/__init__.py index 6671fa1..cce6df9 100644 --- a/src/modules/__init__.py +++ b/src/modules/__init__.py @@ -13,3 +13,4 @@ from .EventsSystem import EventsSystem from .PluginsLoader import PluginsLoader from .WebAPISystem import web_app from .WebAPISystem import _stop as stop_web +from .RateLimiter import RateLimiter diff --git a/src/modules/i18n/__init__.py b/src/modules/i18n/__init__.py index a54bcc5..425114b 100644 --- a/src/modules/i18n/__init__.py +++ b/src/modules/i18n/__init__.py @@ -1,9 +1,152 @@ # -*- coding: utf-8 -*- # Developed by KuiToi Dev -# File modules.i18n.__init__.py +# File modules.i18n # Written by: SantaSpeen -# Version 1.0 +# Version 1.3 # Licence: FPA # (c) kuitoi.su 2023 -from .i18n import MultiLanguage +import builtins +import json +import os +from json import JSONDecodeError + +from core.utils import get_logger + + +class i18n: + data = {} + + def __init__(self, data): + i18n.data = data + + def __getattribute__(self, key): + return i18n.data[key] + + +class MultiLanguage: + + def __init__(self, language: str = None, files_dir="translates/", encoding=None): + if encoding is None: + encoding = config.enc + if language is None: + language = "en" + self.__data = { + "hello": "Hello from KuiToi-Server!", + "config_path": "Use {} to configure.", + "init_ok": "Initialization completed.", + "start": "Server started!", + "stop": "Server stopped!", + "auth_need_key": "BeamMP key is required to run!", + "auth_empty_key": "BeamMP key is empty!", + "auth_cannot_open_browser": "Failed to open browser: {}", + "auth_use_link": "Use this link: {}", + "GUI_yes": "Yes", + "GUI_no": "No", + "GUI_ok": "OK", + "GUI_cancel": "Cancel", + "GUI_need_key_message": "BeamMP key is required to run!\nDo you want to open the link in your browser to get the key?", + "GUI_enter_key_message": "Please enter the key:", + "GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}", + "web_start": "WebAPI started on {} (CTRL+C to stop)", + "core_bind_failed": "Failed to bind port. Error: {}", + "core_direct_mode": "Server started in direct connection mode.", + "core_auth_server_error": "Received invalid response from BeamMP authentication server.", + "core_auth_server_refused": "The BeamMP authentication server refused your key. Reason: {}", + "core_auth_server_refused_no_reason": "The BeamMP authentication server did not provide a reason.", + "core_auth_server_refused_direct_node": "The server is still running, but in direct connection mode.", + "core_auth_server_no_response": "Failed to authenticate the server.", + "core_mods_loaded": "Loaded {} mods. {}Mb", + "core_identifying_connection": "Processing new connection...", + "core_player_kick_outdated": "Incorrect version of BeamMP.", + "core_player_kick_bad_key": "Invalid key passed!", + "core_player_kick_invalid_key": "Invalid key! Please restart your game.", + "core_player_kick_auth_server_fail": "BeamMP authentication server failed! Please try to connect again in 5 minutes.", + "core_player_kick_stale": "Stale client. (Replaced by new connection)", + "core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.", + "core_player_kick_server_full": "Server is full.", + "core_player_set_id": "Player set ID {}", + "core_identifying_okay": "Successful login.", + "game_welcome_message": "Welcome {}!", + "client_mod_request": "Requested mod: {}", + "client_mod_sent": "Mod sent: Size: {}mb, Speed: {}Mb/s ({}sec)", + "client_mod_sent_limit": " (limit {}Mb/s)", + "client_mod_sent_error": "Error sending mod: {}", + "client_sync_time": "Sync time {}sec.", + "client_kicked": "Kicked for reason: \"{}\"", + "client_event_invalid_data": "Invalid data returned from event: {}", + "client_player_disconnected": "Left the server. Playtime: {} min", + "events_not_callable": "Unable to add event \"{}\". Use \"{}\" instead. Skipping...", + "events_not_found": "Event \"{}\" is not registered. Maybe {}? Skipping...", + "events_calling_error": "Error calling \"{}\" in function \"{}\".", + "events_lua_function_not_found": "Unable to call {}lua event - \"{}\" not found.", + "events_lua_local": "local ", + "events_lua_calling_error": "Error: \"{}\" - calling lua event \"{}\", function: \"{}\", arguments: {}", + "plugins_not_found_load": "Function \"def load():\" not found.", + "plugins_not_found_start": "Function \"def start():\" not found.", + "plugins_not_found_unload": "Function \"def unload():\" not found.", + "plugins_kt_invalid": "\"kt\" variable does not belong to the KuiToi class.", + "plugins_invalid": "Plugin \"{}\" cannot be run in KuiToi.", + "plugins_error_loading": "An error occurred while loading the plugin {}: {}", + "plugins_lua_enabled": "You have enabled Lua plugin support.", + "plugins_lua_nuances_warning": "There are some nuances when working with Kuiti. If you have a suggestion for their solution, and it is related to KuiToi, please contact the developer.", + "plugins_lua_legacy_config_create_warning": "Some BeamMP plugins require a properly configured ServerConfig.toml file to function.", + "plugins_lua_legacy_config_create": "Creating it.", + "plugins_lua_unload": "Stopping Lua plugin: {}", + "man_message_man": "man - Shows the help page for COMMAND.\nUsage: man COMMAND", + "help_message_man": "Shows the help page for COMMAND.", + "man_for": "Help page for", + "man_message_not_found": "man: Help page not found.", + "man_command_not_found": "man: Command \"{}\" not found!", + "man_message_help": "help - Shows the names and brief descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands, with a brief description for each command.", + "help_message_help": "Shows the names and brief descriptions of commands", + "help_command": "Command", + "help_message": "Text", + "help_message_not_found": "No text found", + "man_message_stop": "stop - Stops the server.\nUsage: stop", + "help_message_stop": "Stops the server.", + "man_message_exit": "exit - Stops the server.\nUsage: exit", + "help_message_exit": "Stops the server." + } + self.__en_data = self.__data.copy() + self.__i18n = None + self.__encoding = encoding + self.language = language + if not os.path.exists(files_dir): + os.makedirs(files_dir) + if not os.path.exists(files_dir + "en.json"): + with open(files_dir + "en.json", "w") as f: + f.write(json.dumps(self.__en_data, indent=2)) + self.files_dir = files_dir + self.log = get_logger("i18n") + self.fi = False + self.set_language(language) + + def set_language(self, language="en"): + if self.language == language and self.fi: + return + else: + self.fi = True + self.log.debug(f"set_language({language})") + self.language = language + self.open_file() + self.__i18n = i18n(self.__data) + + def open_file(self): + self.log.debug("open_file") + file = self.files_dir + self.language + ".json" + try: + with open(file, encoding=self.__encoding) as f: + self.__data.update(json.load(f)) + return + except JSONDecodeError: + self.log.error( + f"Localisation \"{file}\" have JsonDecodeError. Using default localisation: en.") + except FileNotFoundError: + self.log.warning(f"Localisation \"{file}\" not found; Using default localisation: en.") + self.set_language("en") + + def builtins_hook(self) -> None: + self.log.debug("used builtins_hook") + builtins.i18n = self.__i18n + builtins.i18n_data = self.__data diff --git a/src/modules/i18n/i18n.py b/src/modules/i18n/i18n.py deleted file mode 100644 index 1bfba55..0000000 --- a/src/modules/i18n/i18n.py +++ /dev/null @@ -1,152 +0,0 @@ -# -*- coding: utf-8 -*- - -# Developed by KuiToi Dev -# File modules.i18n.i18n.py -# Written by: SantaSpeen -# Version 1.3 -# Licence: FPA -# (c) kuitoi.su 2023 -import builtins -import json -import os -from json import JSONDecodeError - -from core.utils import get_logger - - -class i18n: - data = {} - - def __init__(self, data): - i18n.data = data - - def __getattribute__(self, key): - return i18n.data[key] - - -class MultiLanguage: - - def __init__(self, language: str = None, files_dir="translates/", encoding=None): - if encoding is None: - encoding = config.enc - if language is None: - language = "en" - self.__data = { - "hello": "Hello from KuiToi-Server!", - "config_path": "Use {} to configure.", - "init_ok": "Initialization completed.", - "start": "Server started!", - "stop": "Server stopped!", - "auth_need_key": "BeamMP key is required to run!", - "auth_empty_key": "BeamMP key is empty!", - "auth_cannot_open_browser": "Failed to open browser: {}", - "auth_use_link": "Use this link: {}", - "GUI_yes": "Yes", - "GUI_no": "No", - "GUI_ok": "OK", - "GUI_cancel": "Cancel", - "GUI_need_key_message": "BeamMP key is required to run!\nDo you want to open the link in your browser to get the key?", - "GUI_enter_key_message": "Please enter the key:", - "GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}", - "web_start": "WebAPI started on {} (CTRL+C to stop)", - "core_bind_failed": "Failed to bind port. Error: {}", - "core_direct_mode": "Server started in direct connection mode.", - "core_auth_server_error": "Received invalid response from BeamMP authentication server.", - "core_auth_server_refused": "The BeamMP authentication server refused your key. Reason: {}", - "core_auth_server_refused_no_reason": "The BeamMP authentication server did not provide a reason.", - "core_auth_server_refused_direct_node": "The server is still running, but in direct connection mode.", - "core_auth_server_no_response": "Failed to authenticate the server.", - "core_mods_loaded": "Loaded {} mods. {}Mb", - "core_identifying_connection": "Processing new connection...", - "core_player_kick_outdated": "Incorrect version of BeamMP.", - "core_player_kick_bad_key": "Invalid key passed!", - "core_player_kick_invalid_key": "Invalid key! Please restart your game.", - "core_player_kick_auth_server_fail": "BeamMP authentication server failed! Please try to connect again in 5 minutes.", - "core_player_kick_stale": "Stale client. (Replaced by new connection)", - "core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.", - "core_player_kick_server_full": "Server is full.", - "core_player_set_id": "Player set ID {}", - "core_identifying_okay": "Successful login.", - "game_welcome_message": "Welcome {}!", - "client_mod_request": "Requested mod: {}", - "client_mod_sent": "Mod sent: Size: {}mb, Speed: {}Mb/s ({}sec)", - "client_mod_sent_limit": " (limit {}Mb/s)", - "client_mod_sent_error": "Error sending mod: {}", - "client_sync_time": "Sync time {}sec.", - "client_kicked": "Kicked for reason: \"{}\"", - "client_event_invalid_data": "Invalid data returned from event: {}", - "client_player_disconnected": "Left the server. Playtime: {} min", - "events_not_callable": "Unable to add event \"{}\". Use \"{}\" instead. Skipping...", - "events_not_found": "Event \"{}\" is not registered. Maybe {}? Skipping...", - "events_calling_error": "Error calling \"{}\" in function \"{}\".", - "events_lua_function_not_found": "Unable to call {}lua event - \"{}\" not found.", - "events_lua_local": "local ", - "events_lua_calling_error": "Error: \"{}\" - calling lua event \"{}\", function: \"{}\", arguments: {}", - "plugins_not_found_load": "Function \"def load():\" not found.", - "plugins_not_found_start": "Function \"def start():\" not found.", - "plugins_not_found_unload": "Function \"def unload():\" not found.", - "plugins_kt_invalid": "\"kt\" variable does not belong to the KuiToi class.", - "plugins_invalid": "Plugin \"{}\" cannot be run in KuiToi.", - "plugins_error_loading": "An error occurred while loading the plugin {}: {}", - "plugins_lua_enabled": "You have enabled Lua plugin support.", - "plugins_lua_nuances_warning": "There are some nuances when working with Kuiti. If you have a suggestion for their solution, and it is related to KuiToi, please contact the developer.", - "plugins_lua_legacy_config_create_warning": "Some BeamMP plugins require a properly configured ServerConfig.toml file to function.", - "plugins_lua_legacy_config_create": "Creating it.", - "plugins_lua_unload": "Stopping Lua plugin: {}", - "man_message_man": "man - Shows the help page for COMMAND.\nUsage: man COMMAND", - "help_message_man": "Shows the help page for COMMAND.", - "man_for": "Help page for", - "man_message_not_found": "man: Help page not found.", - "man_command_not_found": "man: Command \"{}\" not found!", - "man_message_help": "help - Shows the names and brief descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands, with a brief description for each command.", - "help_message_help": "Shows the names and brief descriptions of commands", - "help_command": "Command", - "help_message": "Text", - "help_message_not_found": "No text found", - "man_message_stop": "stop - Stops the server.\nUsage: stop", - "help_message_stop": "Stops the server.", - "man_message_exit": "exit - Stops the server.\nUsage: exit", - "help_message_exit": "Stops the server." - } - self.__en_data = self.__data.copy() - self.__i18n = None - self.__encoding = encoding - self.language = language - if not os.path.exists(files_dir): - os.makedirs(files_dir) - if not os.path.exists(files_dir + "en.json"): - with open(files_dir + "en.json", "w") as f: - f.write(json.dumps(self.__en_data, indent=2)) - self.files_dir = files_dir - self.log = get_logger("i18n") - self.fi = False - self.set_language(language) - - def set_language(self, language="en"): - if self.language == language and self.fi: - return - else: - self.fi = True - self.log.debug(f"set_language({language})") - self.language = language - self.open_file() - self.__i18n = i18n(self.__data) - - def open_file(self): - self.log.debug("open_file") - file = self.files_dir + self.language + ".json" - try: - with open(file, encoding=self.__encoding) as f: - self.__data.update(json.load(f)) - return - except JSONDecodeError: - self.log.error( - f"Localisation \"{file}\" have JsonDecodeError. Using default localisation: en.") - except FileNotFoundError: - self.log.warning(f"Localisation \"{file}\" not found; Using default localisation: en.") - self.set_language("en") - - def builtins_hook(self) -> None: - self.log.debug("used builtins_hook") - builtins.i18n = self.__i18n - builtins.i18n_data = self.__data diff --git a/src/modules/i18n/i18n-builtins.pyi b/src/modules/i18n/readme.md similarity index 98% rename from src/modules/i18n/i18n-builtins.pyi rename to src/modules/i18n/readme.md index d406264..5509298 100644 --- a/src/modules/i18n/i18n-builtins.pyi +++ b/src/modules/i18n/readme.md @@ -1,3 +1,6 @@ +### Builtins + +```python class i18n: # Basic phases hello: str @@ -107,3 +110,5 @@ class i18n: # Command: exit man_message_exit: str help_message_exit: str + +``` \ No newline at end of file