mirror of
https://gitflic.ru/project/alt-gnome/karkas.git
synced 2025-10-06 21:06:06 +03:00
Переименование файлов и директорий
This commit is contained in:
23
src/karkas_core/README.md
Normal file
23
src/karkas_core/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Karkas Core
|
||||
|
||||
Это ядро Karkas, содержащее базовые компоненты:
|
||||
|
||||
- Система управления модулями.
|
||||
- Логирование.
|
||||
- Утилиты.
|
||||
|
||||
## Система управления модулями
|
||||
|
||||
Система управления модулями отвечает за:
|
||||
|
||||
- Загрузку модулей.
|
||||
- Проверку зависимостей.
|
||||
- Предоставление API для взаимодействия между модулями.
|
||||
|
||||
## Логирование
|
||||
|
||||
Модуль логирования предоставляет функции для записи логов в консоль.
|
||||
|
||||
## Утилиты
|
||||
|
||||
Модуль утилит содержит вспомогательные функции, например, для форматирования текста.
|
1
src/karkas_core/karkas_core/__init__.py
Normal file
1
src/karkas_core/karkas_core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .main import Karkas
|
38
src/karkas_core/karkas_core/lib.py
Normal file
38
src/karkas_core/karkas_core/lib.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import importlib
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.types import Update
|
||||
|
||||
|
||||
def get_module_directory(module_name):
|
||||
spec = importlib.util.find_spec(module_name)
|
||||
if spec is None:
|
||||
raise ImportError(f"Module {module_name} not found")
|
||||
module_path = spec.origin
|
||||
if module_path is None:
|
||||
raise ImportError(f"Module {module_name} has no origin path")
|
||||
return os.path.dirname(module_path)
|
||||
|
||||
|
||||
try:
|
||||
from fastapi import FastAPI, Request
|
||||
|
||||
async def register_bot_webhook(app: FastAPI, bot: Bot, dp: Dispatcher):
|
||||
async def handle_webhook(request: Request):
|
||||
try:
|
||||
update = Update.model_validate(
|
||||
await request.json(), context={"bot": bot}
|
||||
)
|
||||
await dp.feed_update(bot, update)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return {"ok": False}
|
||||
|
||||
return {"ok": True}
|
||||
|
||||
app.post("/webhook")(handle_webhook)
|
||||
|
||||
except ImportError:
|
||||
pass
|
46
src/karkas_core/karkas_core/logger.py
Normal file
46
src/karkas_core/karkas_core/logger.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
app_logger = logging.getLogger("karkas")
|
||||
log_level = logging.INFO
|
||||
|
||||
|
||||
def patch_logger(logger_: logging.Logger):
|
||||
logger_.handlers = []
|
||||
formatter = logging.Formatter("%(asctime)s %(message)s", datefmt="%H:%M:%S")
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(formatter)
|
||||
logger_.addHandler(console_handler)
|
||||
logger_.propagate = False
|
||||
logger_.setLevel(log_level)
|
||||
return logger_
|
||||
|
||||
|
||||
def setup_logger():
|
||||
"""
|
||||
Настройка логирования
|
||||
"""
|
||||
patch_logger(app_logger)
|
||||
|
||||
|
||||
def log(message):
|
||||
if isinstance(message, Exception):
|
||||
error_message = f"Error: {str(message)}\n{traceback.format_exc()}"
|
||||
app_logger.error(error_message)
|
||||
else:
|
||||
app_logger.info(message)
|
||||
|
||||
|
||||
try:
|
||||
from hypercorn.logging import Logger as HypercornLogger
|
||||
|
||||
class CustomLogger(HypercornLogger):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.error_logger:
|
||||
patch_logger(self.error_logger)
|
||||
if self.access_logger:
|
||||
patch_logger(self.access_logger)
|
||||
|
||||
except ImportError:
|
||||
pass
|
120
src/karkas_core/karkas_core/main.py
Normal file
120
src/karkas_core/karkas_core/main.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
|
||||
from karkas_core.lib import register_bot_webhook
|
||||
from karkas_core.logger import CustomLogger, log, setup_logger
|
||||
from karkas_core.modules_system import ModulesManager
|
||||
from karkas_core.modules_system.public_api import get_module
|
||||
from karkas_core.singleton import Singleton
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from karkas_blocks.standard.config import IConfig
|
||||
|
||||
|
||||
class Karkas:
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
async def init_app(self, bot_modules, metainfo=None):
|
||||
setup_logger()
|
||||
singleton = Singleton()
|
||||
if isinstance(metainfo, dict):
|
||||
singleton.storage["metainfo"] = metainfo
|
||||
|
||||
try:
|
||||
singleton.modules_manager = ModulesManager()
|
||||
|
||||
for block_loader in bot_modules:
|
||||
info = block_loader.info()
|
||||
log(f"Loading {info.name} ({info.id}) module")
|
||||
await singleton.modules_manager.load(block_loader)
|
||||
|
||||
register_config()
|
||||
|
||||
config: "IConfig" = get_module("standard.config", "config")
|
||||
|
||||
config.load()
|
||||
|
||||
singleton.bot = Bot(token=config.get("core::token"))
|
||||
singleton.dp = Dispatcher(storage=singleton.storage["_fsm_storage"])
|
||||
singleton.dp.include_routers(*singleton.storage["_routers"])
|
||||
|
||||
for middleware in singleton.storage["_outer_message_middlewares"]:
|
||||
singleton.dp.message.outer_middleware.register(middleware)
|
||||
|
||||
await singleton.modules_manager.late_init()
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
async def start(self):
|
||||
config: "IConfig" = get_module("standard.config", "config")
|
||||
|
||||
if config.get("core::mode") == "WEBHOOK":
|
||||
await self.start_webhook_mode()
|
||||
else:
|
||||
await self.start_long_polling_mode()
|
||||
|
||||
return
|
||||
|
||||
async def start_long_polling_mode(self):
|
||||
singleton = Singleton()
|
||||
await singleton.bot.delete_webhook()
|
||||
await singleton.dp.start_polling(singleton.bot)
|
||||
|
||||
async def start_webhook_mode(self):
|
||||
try:
|
||||
from fastapi import FastAPI
|
||||
from hypercorn.asyncio import serve
|
||||
from hypercorn.config import Config as HyperConfig
|
||||
except ImportError:
|
||||
log(
|
||||
"Error: FastAPI and Hypercorn are required"
|
||||
"for webhook mode. Please install them."
|
||||
)
|
||||
return
|
||||
|
||||
singleton = Singleton()
|
||||
app = FastAPI()
|
||||
config = get_module("standard.config", "config")
|
||||
app.mount(config.get("miniapp::prefix"), singleton.storage["webapp"])
|
||||
await register_bot_webhook(app, singleton.bot, singleton.dp)
|
||||
await singleton.bot.set_webhook(config.get("core::webhook::public_url"))
|
||||
hyperConfig = HyperConfig()
|
||||
hyperConfig.bind = [f"0.0.0.0:{config.get("core::webhook::port")}"]
|
||||
hyperConfig.logger_class = CustomLogger
|
||||
await serve(app, hyperConfig)
|
||||
|
||||
|
||||
def register_config():
|
||||
config: "IConfig" = get_module("standard.config", "config")
|
||||
|
||||
config.register(
|
||||
"core::token",
|
||||
"password",
|
||||
visible=False,
|
||||
)
|
||||
|
||||
config.register(
|
||||
"core::mode",
|
||||
"select",
|
||||
options=["WEBHOOK", "LONG_POLLING"],
|
||||
default_value="WEBHOOK",
|
||||
visible=False,
|
||||
)
|
||||
|
||||
config.register(
|
||||
"core::webhook::port",
|
||||
"int",
|
||||
default_value=9000,
|
||||
visible=False,
|
||||
)
|
||||
|
||||
config.register(
|
||||
"core::webhook::public_url",
|
||||
"string",
|
||||
visible=False,
|
||||
)
|
1
src/karkas_core/karkas_core/modules_system/__init__.py
Normal file
1
src/karkas_core/karkas_core/modules_system/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .modules_manager import ModulesManager
|
@@ -0,0 +1,2 @@
|
||||
from .fs_loader import FSLoader
|
||||
from .unsafe_fs_loader import UnsafeFSLoader
|
43
src/karkas_core/karkas_core/modules_system/loaders/base.py
Normal file
43
src/karkas_core/karkas_core/modules_system/loaders/base.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import types
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from dataclasses_json import dataclass_json
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class DependencyInfo:
|
||||
version: str
|
||||
uses: Optional[List[str]] = None
|
||||
|
||||
|
||||
DependencyType = Union[str, DependencyInfo]
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class Dependencies:
|
||||
required: Optional[Dict[str, DependencyType]] = None
|
||||
optional: Optional[Dict[str, DependencyType]] = None
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class ModuleInfo:
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
version: str
|
||||
author: Union[str, List[str]]
|
||||
privileged: bool
|
||||
dependencies: Dependencies
|
||||
pythonDependencies: Optional[Dependencies] = None
|
||||
|
||||
|
||||
class AbstractLoader:
|
||||
def info(self) -> ModuleInfo:
|
||||
raise NotImplementedError
|
||||
|
||||
def load(self) -> types.ModuleType:
|
||||
raise NotImplementedError
|
@@ -0,0 +1,91 @@
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
from RestrictedPython import compile_restricted_exec
|
||||
|
||||
# from karkas_core.logger import log
|
||||
from karkas_core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader
|
||||
from karkas_core.modules_system.safe.policy import (
|
||||
ALLOWED_IMPORTS,
|
||||
BUILTINS,
|
||||
RestrictedPythonPolicy,
|
||||
)
|
||||
|
||||
|
||||
class FSLoader(UnsafeFSLoader):
|
||||
def __init__(self, path):
|
||||
super().__init__(path)
|
||||
self.builtins = BUILTINS.copy()
|
||||
self.builtins["__import__"] = self._hook_import
|
||||
self.module_info = self.info()
|
||||
self.allowed_python_dependencies = self._get_allowed_python_dependencies()
|
||||
|
||||
def load(self):
|
||||
if self.module_info.privileged:
|
||||
raise Exception("Only non privileged modules are allowed to be imported")
|
||||
self.module_id = self.module_info.id
|
||||
|
||||
return self._hook_import(".")
|
||||
|
||||
def _get_allowed_python_dependencies(self):
|
||||
allowed = {}
|
||||
|
||||
if self.module_info.pythonDependencies:
|
||||
if self.module_info.pythonDependencies.required:
|
||||
allowed.update(self.module_info.pythonDependencies.required)
|
||||
if self.module_info.pythonDependencies.optional:
|
||||
allowed.update(self.module_info.pythonDependencies.optional)
|
||||
|
||||
for allowed_module in ALLOWED_IMPORTS:
|
||||
allowed[allowed_module] = "*"
|
||||
|
||||
return allowed
|
||||
|
||||
def _resolve_module_from_path(self, module_name: str):
|
||||
path = Path(self.path)
|
||||
|
||||
if module_name != ".":
|
||||
path = path.joinpath(module_name.replace(".", "/"))
|
||||
|
||||
if path.is_dir():
|
||||
init_file_path = path / "__init__.py"
|
||||
if not init_file_path.exists():
|
||||
raise FileNotFoundError(f"File {init_file_path} does not exist.")
|
||||
file_path = init_file_path
|
||||
else:
|
||||
path = path.with_suffix(".py")
|
||||
if path.is_file():
|
||||
file_path = path
|
||||
else:
|
||||
raise ValueError(f"Module not found: {module_name}")
|
||||
|
||||
return file_path
|
||||
|
||||
def _hook_import(self, name: str, *args, **kwargs):
|
||||
if name == "karkas_core.modules_system.public_api":
|
||||
module = __import__(name, *args, **kwargs)
|
||||
module.__karkas_block_id__ = self.module_id
|
||||
return module
|
||||
|
||||
for key in self.allowed_python_dependencies.keys():
|
||||
if name == key or name.startswith(f"{key}."):
|
||||
return __import__(name, *args, **kwargs)
|
||||
|
||||
module_file_path = self._resolve_module_from_path(name)
|
||||
|
||||
with open(module_file_path, "r") as f:
|
||||
src = f.read()
|
||||
|
||||
module = types.ModuleType(name)
|
||||
module.__dict__.update(
|
||||
{"__builtins__": self.builtins, "__karkas_block_id__": self.module_id}
|
||||
)
|
||||
result = compile_restricted_exec(src, "<string>", policy=RestrictedPythonPolicy)
|
||||
|
||||
if result.errors:
|
||||
for error in result.errors:
|
||||
print(error)
|
||||
|
||||
exec(result.code, module.__dict__) # nosec
|
||||
|
||||
return module
|
@@ -0,0 +1 @@
|
||||
from .FSLoader import FSLoader
|
@@ -0,0 +1,61 @@
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from karkas_core.modules_system.loaders.base import AbstractLoader, ModuleInfo
|
||||
|
||||
|
||||
class UnsafeFSLoader(AbstractLoader):
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
def info(self):
|
||||
with open(os.path.join(self.path, "info.json"), "r") as f:
|
||||
return ModuleInfo.from_json(f.read())
|
||||
|
||||
def _resolve_module_from_path(self, module_name: str):
|
||||
path = Path(self.path)
|
||||
|
||||
if module_name != ".":
|
||||
path = path.joinpath(module_name.replace(".", "/"))
|
||||
|
||||
if path.is_dir():
|
||||
init_file_path = path / "__init__.py"
|
||||
if not init_file_path.exists():
|
||||
raise FileNotFoundError(f"File {init_file_path} does not exist.")
|
||||
file_path = init_file_path
|
||||
else:
|
||||
path = path.with_suffix(".py")
|
||||
if path.is_file():
|
||||
file_path = path
|
||||
else:
|
||||
raise ValueError(f"Module not found: {module_name}")
|
||||
|
||||
return file_path
|
||||
|
||||
def load(self):
|
||||
self.info()
|
||||
|
||||
full_path = self._resolve_module_from_path(".")
|
||||
|
||||
if full_path.name == "__init__.py":
|
||||
module_name = full_path.parent.name
|
||||
path = full_path.parent.parent.absolute()
|
||||
else:
|
||||
module_name = full_path.stem
|
||||
path = full_path.parent.absolute()
|
||||
|
||||
# Добавляем директорию модуля в sys.path
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
# Загружаем спецификацию модуля
|
||||
spec = importlib.util.spec_from_file_location(module_name, full_path)
|
||||
|
||||
# Создаем модуль
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
|
||||
# Выполняем модуль
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
return module
|
@@ -0,0 +1 @@
|
||||
from .UnsafeFSLoader import UnsafeFSLoader
|
169
src/karkas_core/karkas_core/modules_system/modules_manager.py
Normal file
169
src/karkas_core/karkas_core/modules_system/modules_manager.py
Normal file
@@ -0,0 +1,169 @@
|
||||
import importlib
|
||||
import inspect
|
||||
|
||||
import pkg_resources
|
||||
import semver
|
||||
|
||||
from karkas_core.modules_system.loaders.base import (
|
||||
AbstractLoader,
|
||||
DependencyInfo,
|
||||
ModuleInfo,
|
||||
)
|
||||
|
||||
|
||||
def is_version_compatible(version, requirement):
|
||||
def parse_requirement(req):
|
||||
if req.startswith("^"):
|
||||
base_version = req[1:]
|
||||
base_version_info = semver.VersionInfo.parse(base_version)
|
||||
range_start = base_version_info
|
||||
range_end = base_version_info.bump_major()
|
||||
return [f">={range_start}", f"<{range_end}"]
|
||||
else:
|
||||
return [req]
|
||||
|
||||
for r in parse_requirement(requirement):
|
||||
if r == "*":
|
||||
continue
|
||||
|
||||
if not semver.Version.parse(version).match(r):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_python_dependencies(info: ModuleInfo):
|
||||
if info.pythonDependencies and info.pythonDependencies.required:
|
||||
for dependency, req in info.pythonDependencies.required.items():
|
||||
try:
|
||||
importlib.import_module(dependency)
|
||||
except ImportError:
|
||||
raise Exception(
|
||||
f"Module {info.id} requires {dependency}, "
|
||||
f"but it is not installed"
|
||||
)
|
||||
|
||||
try:
|
||||
installed_version = pkg_resources.get_distribution(dependency).version
|
||||
except pkg_resources.DistributionNotFound:
|
||||
installed_version = "*"
|
||||
|
||||
if isinstance(req, str):
|
||||
required_version = req
|
||||
elif isinstance(req, DependencyInfo):
|
||||
required_version = req.version
|
||||
else:
|
||||
raise ValueError(f"Invalid dependency specification for {dependency}")
|
||||
|
||||
if not is_version_compatible(installed_version, required_version):
|
||||
raise Exception(
|
||||
f"Module {info.id} depends on {dependency} {required_version}, "
|
||||
f"but version {installed_version} is installed"
|
||||
)
|
||||
|
||||
|
||||
def check_dependency_uses(
|
||||
loaded_dependency, required_uses, dependent_module_id, dependency_id
|
||||
):
|
||||
module = loaded_dependency.get("module")
|
||||
if not module:
|
||||
raise Exception(f"Module object not found for dependency {dependency_id}")
|
||||
|
||||
for required_attr in required_uses:
|
||||
if not hasattr(module, required_attr):
|
||||
raise Exception(
|
||||
f"Module {dependent_module_id} requires '{required_attr}' "
|
||||
f"from {dependency_id}, but it is not available"
|
||||
)
|
||||
|
||||
|
||||
async def await_if_async(module, method_name):
|
||||
if hasattr(module, method_name):
|
||||
method = getattr(module, method_name)
|
||||
if inspect.iscoroutinefunction(method):
|
||||
await method()
|
||||
else:
|
||||
method()
|
||||
|
||||
|
||||
class ModulesManager:
|
||||
def __init__(self):
|
||||
self.modules = []
|
||||
|
||||
async def load(self, loader: AbstractLoader):
|
||||
info = loader.info()
|
||||
|
||||
# Check if the module is already loaded
|
||||
if any(mod["info"].id == info.id for mod in self.modules):
|
||||
return
|
||||
|
||||
self.check_module_dependencies(info)
|
||||
check_python_dependencies(info)
|
||||
|
||||
module_info = {
|
||||
"info": info,
|
||||
"module": None,
|
||||
}
|
||||
self.modules.append(module_info)
|
||||
module = loader.load()
|
||||
module_info["module"] = module
|
||||
|
||||
await await_if_async(module, "module_init")
|
||||
|
||||
def check_module_dependencies(self, info: ModuleInfo):
|
||||
if info.dependencies.required:
|
||||
for dependency, req in info.dependencies.required.items():
|
||||
loaded_dependency = next(
|
||||
(mod for mod in self.modules if mod["info"].id == dependency), None
|
||||
)
|
||||
if not loaded_dependency:
|
||||
raise Exception(
|
||||
f"Module {info.id} depends on {dependency},"
|
||||
f"but it is not loaded"
|
||||
)
|
||||
|
||||
loaded_dependency_info = loaded_dependency["info"]
|
||||
|
||||
if isinstance(req, str):
|
||||
required_version = req
|
||||
elif isinstance(req, DependencyInfo):
|
||||
required_version = req.version
|
||||
if req.uses:
|
||||
check_dependency_uses(
|
||||
loaded_dependency, req.uses, info.id, dependency
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid dependency specification for {dependency}"
|
||||
)
|
||||
|
||||
if not is_version_compatible(
|
||||
loaded_dependency_info.version, required_version
|
||||
):
|
||||
raise Exception(
|
||||
f"Module {info.id} depends on {dependency} {required_version}, "
|
||||
f"but version {loaded_dependency_info.version} is loaded"
|
||||
)
|
||||
|
||||
async def late_init(self):
|
||||
for m in self.modules:
|
||||
module = m["module"]
|
||||
await await_if_async(module, "module_late_init")
|
||||
|
||||
def get_by_id(self, module_id: str):
|
||||
module = next(
|
||||
(mod for mod in self.modules if mod["info"].id == module_id), None
|
||||
)
|
||||
if not module:
|
||||
raise Exception(f"Module with id {module_id} not loaded")
|
||||
|
||||
return module["module"]
|
||||
|
||||
def get_info_by_id(self, module_id: str):
|
||||
module = next(
|
||||
(mod for mod in self.modules if mod["info"].id == module_id), None
|
||||
)
|
||||
if not module:
|
||||
raise Exception(f"Module with id {module_id} not loaded")
|
||||
|
||||
return module["info"]
|
@@ -0,0 +1,13 @@
|
||||
from karkas_core.logger import log
|
||||
|
||||
from .public_api import (
|
||||
Storage,
|
||||
get_fsm_context,
|
||||
get_metainfo,
|
||||
get_module,
|
||||
register_outer_message_middleware,
|
||||
register_router,
|
||||
set_chat_menu_button,
|
||||
set_my_commands,
|
||||
)
|
||||
from .utils import Utils
|
@@ -0,0 +1,136 @@
|
||||
import inspect
|
||||
import types
|
||||
from typing import Any, Tuple, Union
|
||||
|
||||
from aiogram import BaseMiddleware, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.storage.base import StorageKey
|
||||
|
||||
# from karkas_core.logger import log
|
||||
from karkas_core.modules_system.loaders.base import DependencyInfo
|
||||
from karkas_core.singleton import Singleton
|
||||
|
||||
|
||||
async def set_chat_menu_button(menu_button):
|
||||
app = Singleton()
|
||||
await app.bot.set_chat_menu_button(menu_button=menu_button)
|
||||
|
||||
|
||||
def register_router(router: Router):
|
||||
app = Singleton()
|
||||
app.storage["_routers"].append(router)
|
||||
|
||||
|
||||
def register_outer_message_middleware(middleware: BaseMiddleware):
|
||||
app = Singleton()
|
||||
app.storage["_outer_message_middlewares"].append(middleware)
|
||||
|
||||
|
||||
def get_metainfo():
|
||||
app = Singleton()
|
||||
return app.storage["metainfo"].copy()
|
||||
|
||||
|
||||
async def set_my_commands(commands):
|
||||
app = Singleton()
|
||||
await app.bot.set_my_commands(commands)
|
||||
|
||||
|
||||
async def get_fsm_context(chat_id: int, user_id: int) -> FSMContext:
|
||||
dp = Singleton().dp
|
||||
bot = Singleton().bot
|
||||
|
||||
return FSMContext(
|
||||
storage=dp.storage,
|
||||
key=StorageKey(
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
bot_id=bot.id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def set_fsm(storage):
|
||||
app = Singleton()
|
||||
app.storage["_fsm_storage"] = storage
|
||||
|
||||
|
||||
def get_module(
|
||||
module_id: str, paths=None
|
||||
) -> Union[types.ModuleType, Union[Any, None], Tuple[Union[Any, None], ...]]:
|
||||
|
||||
caller_globals = inspect.currentframe().f_back.f_globals
|
||||
app = Singleton()
|
||||
|
||||
allowed_uses = None
|
||||
|
||||
if "__karkas_block_id__" in caller_globals:
|
||||
caller_module_id = caller_globals["__karkas_block_id__"]
|
||||
caller_module_info = app.modules_manager.get_info_by_id(caller_module_id)
|
||||
|
||||
if caller_module_info and caller_module_info.dependencies:
|
||||
dependency = None
|
||||
if caller_module_info.dependencies.required:
|
||||
dependency = caller_module_info.dependencies.required.get(module_id)
|
||||
if not dependency and caller_module_info.dependencies.optional:
|
||||
dependency = caller_module_info.dependencies.optional.get(module_id)
|
||||
|
||||
if (
|
||||
dependency
|
||||
and isinstance(dependency, DependencyInfo)
|
||||
and dependency.uses
|
||||
):
|
||||
allowed_uses = set(dependency.uses)
|
||||
|
||||
module = app.modules_manager.get_by_id(module_id)
|
||||
|
||||
if not module:
|
||||
raise ModuleNotFoundError(f"Module {module_id} not found")
|
||||
|
||||
if paths is None:
|
||||
if allowed_uses is not None:
|
||||
raise PermissionError(
|
||||
f"Direct access to module {module_id} is "
|
||||
f"not allowed for {caller_module_id}. Specify allowed attributes."
|
||||
)
|
||||
return module
|
||||
|
||||
if isinstance(paths, str):
|
||||
paths = [paths]
|
||||
|
||||
results = []
|
||||
|
||||
for path in paths:
|
||||
current_obj = module
|
||||
try:
|
||||
parts = path.split(".")
|
||||
for part in parts:
|
||||
if allowed_uses is not None and part not in allowed_uses:
|
||||
raise AttributeError(
|
||||
f"Access to '{part}' is not allowed "
|
||||
+ f"for module {caller_module_id}"
|
||||
)
|
||||
current_obj = getattr(current_obj, part)
|
||||
results.append(current_obj)
|
||||
except AttributeError as e:
|
||||
if "is not allowed" in str(e):
|
||||
raise PermissionError(str(e))
|
||||
results.append(None)
|
||||
|
||||
if len(results) == 1:
|
||||
return results[0]
|
||||
else:
|
||||
return tuple(results)
|
||||
|
||||
|
||||
class Storage:
|
||||
|
||||
@staticmethod
|
||||
def set(key: str, value: Any):
|
||||
storage = Singleton().storage
|
||||
storage[key] = value
|
||||
|
||||
@staticmethod
|
||||
def get(key: str):
|
||||
storage = Singleton().storage
|
||||
return storage.get(key)
|
@@ -0,0 +1,12 @@
|
||||
import re
|
||||
|
||||
CLEAN_HTML = re.compile("<.*?>")
|
||||
|
||||
|
||||
class Utils:
|
||||
@staticmethod
|
||||
def code_format(code: str, lang: str):
|
||||
if lang:
|
||||
return f'<pre><code class="language-{lang}">{code}</code></pre>'
|
||||
else:
|
||||
return f"<pre>{code}</pre>"
|
146
src/karkas_core/karkas_core/modules_system/safe/policy.py
Normal file
146
src/karkas_core/karkas_core/modules_system/safe/policy.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import types
|
||||
from _ast import AnnAssign
|
||||
from typing import Any
|
||||
|
||||
from aiogram import Bot
|
||||
from RestrictedPython import (
|
||||
RestrictingNodeTransformer,
|
||||
limited_builtins,
|
||||
safe_builtins,
|
||||
utility_builtins,
|
||||
)
|
||||
from RestrictedPython.Eval import default_guarded_getitem, default_guarded_getiter
|
||||
from RestrictedPython.Guards import ( # guarded_setattr,; full_write_guard,
|
||||
_write_wrapper,
|
||||
guarded_iter_unpack_sequence,
|
||||
guarded_unpack_sequence,
|
||||
safer_getattr,
|
||||
)
|
||||
|
||||
from karkas_core.logger import log
|
||||
from karkas_core.modules_system.safe.zope_guards import extra_safe_builtins
|
||||
|
||||
|
||||
class RestrictedPythonPolicy(RestrictingNodeTransformer):
|
||||
def visit_AsyncFunctionDef(self, node):
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
def visit_Await(self, node):
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
def visit_AsyncFor(self, node):
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
def visit_AsyncWith(self, node):
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
"""
|
||||
Не работает из-за getattr
|
||||
|
||||
def visit_Match(self, node) -> Any:
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
def visit_match_case(self, node) -> Any:
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
def visit_MatchAs(self, node) -> Any:
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
def visit_MatchValue(self, node) -> Any:
|
||||
return self.node_contents_visit(node)
|
||||
"""
|
||||
|
||||
def visit_AnnAssign(self, node: AnnAssign) -> Any:
|
||||
# missing in RestrictingNodeTransformer
|
||||
# this doesn't need the logic that is in visit_Assign
|
||||
# because it doesn't have a "targets" attribute,
|
||||
# and node.target: Name | Attribute | Subscript
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
# new Python 3.12 nodes
|
||||
def visit_TypeAlias(self, node) -> Any:
|
||||
# missing in RestrictingNodeTransformer
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
def visit_TypeVar(self, node) -> Any:
|
||||
# missing in RestrictingNodeTransformer
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
def visit_TypeVarTuple(self, node) -> Any:
|
||||
# missing in RestrictingNodeTransformer
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
def visit_ParamSpec(self, node) -> Any:
|
||||
# missing in RestrictingNodeTransformer
|
||||
return self.node_contents_visit(node)
|
||||
|
||||
|
||||
def _metaclass(name, bases, dict):
|
||||
ob = type(name, bases, dict)
|
||||
ob.__allow_access_to_unprotected_subobjects__ = 1
|
||||
ob._guarded_writes = 1
|
||||
return ob
|
||||
|
||||
|
||||
ALLOWED_IMPORTS = [
|
||||
"typing",
|
||||
"aiogram",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
|
||||
def safes_getattr(object, name, default=None, getattr=safer_getattr):
|
||||
if isinstance(object, Bot) and name == "token":
|
||||
log("Bot.token is not allowed")
|
||||
raise Exception("Bot.token is not allowed")
|
||||
|
||||
return getattr(object, name, default)
|
||||
|
||||
|
||||
trusted_settters_classes = []
|
||||
|
||||
|
||||
def safes_setattr(self, key, value):
|
||||
if (
|
||||
isinstance(getattr(type(self), key, None), property)
|
||||
and getattr(type(self), key).fset is not None
|
||||
):
|
||||
getattr(type(self), key).fset(self, value)
|
||||
return
|
||||
|
||||
|
||||
def write_guard():
|
||||
# ed scope abuse!
|
||||
# safetypes and Wrapper variables are used by guard()
|
||||
safetypes = {dict, list}
|
||||
Wrapper = _write_wrapper()
|
||||
|
||||
def guard(ob):
|
||||
# Don't bother wrapping simple types, or objects that claim to
|
||||
# handle their own write security.
|
||||
if type(ob) in safetypes or hasattr(ob, "_guarded_writes"):
|
||||
return ob
|
||||
|
||||
if type(ob) in trusted_settters_classes:
|
||||
setattr(ob, "__guarded_setattr__", types.MethodType(safes_setattr, ob))
|
||||
|
||||
# Hand the object to the Wrapper instance, then return the instance.
|
||||
return Wrapper(ob)
|
||||
|
||||
return guard
|
||||
|
||||
|
||||
BUILTINS = safe_builtins.copy()
|
||||
BUILTINS.update(utility_builtins)
|
||||
BUILTINS.update(limited_builtins)
|
||||
BUILTINS.update(extra_safe_builtins)
|
||||
BUILTINS["__metaclass__"] = _metaclass
|
||||
BUILTINS["_getitem_"] = default_guarded_getitem
|
||||
BUILTINS["_getattr_"] = safes_getattr
|
||||
BUILTINS["_getiter_"] = default_guarded_getiter
|
||||
BUILTINS["_write_"] = write_guard()
|
||||
BUILTINS["_iter_unpack_sequence_"] = guarded_iter_unpack_sequence
|
||||
BUILTINS["_unpack_sequence_"] = guarded_unpack_sequence
|
||||
BUILTINS["staticmethod"] = staticmethod
|
||||
BUILTINS["tuple"] = tuple
|
||||
BUILTINS["reversed"] = reversed
|
225
src/karkas_core/karkas_core/modules_system/safe/zope_guards.py
Normal file
225
src/karkas_core/karkas_core/modules_system/safe/zope_guards.py
Normal file
@@ -0,0 +1,225 @@
|
||||
#############################################################################
|
||||
#
|
||||
# Copyright (c) 2024 Karkas Team
|
||||
# Copyright (c) 2002 Zope Foundation and Contributors.
|
||||
#
|
||||
# This software includes a function derived from the software subject to the
|
||||
# provisions of the Zope Public License, Version 2.1 (ZPL). A copy of the ZPL
|
||||
# should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
|
||||
# AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST
|
||||
# INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
extra_safe_builtins = {}
|
||||
|
||||
|
||||
class GuardedDictType:
|
||||
def __call__(self, *args, **kwargs):
|
||||
return dict(*args, **kwargs)
|
||||
|
||||
def fromkeys(self, S, v=None):
|
||||
return dict.fromkeys(S, v)
|
||||
|
||||
|
||||
extra_safe_builtins["dict"] = GuardedDictType()
|
||||
|
||||
|
||||
ContainerAssertions = {
|
||||
type(()): 1,
|
||||
bytes: 1,
|
||||
str: 1,
|
||||
range: 1,
|
||||
}
|
||||
|
||||
Containers = ContainerAssertions.get
|
||||
|
||||
|
||||
def _error(index):
|
||||
raise Exception("unauthorized access to element")
|
||||
|
||||
|
||||
def guard(container, value, index=None):
|
||||
# if Containers(type(container)) and Containers(type(value)):
|
||||
# # Simple type. Short circuit.
|
||||
# return
|
||||
# I don't know how to do this.
|
||||
# if getSecurityManager().validate(container, container, index, value):
|
||||
# return
|
||||
# _error(index)
|
||||
return
|
||||
|
||||
|
||||
class SafeIter:
|
||||
__allow_access_to_unprotected_subobjects__ = 1
|
||||
|
||||
def __init__(self, ob, container=None):
|
||||
self._iter = iter(ob)
|
||||
if container is None:
|
||||
container = ob
|
||||
self.container = container
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
ob = next(self._iter)
|
||||
guard(self.container, ob)
|
||||
return ob
|
||||
|
||||
next = __next__
|
||||
|
||||
|
||||
class NullIter(SafeIter):
|
||||
def __init__(self, ob):
|
||||
self._iter = ob
|
||||
|
||||
def __next__(self):
|
||||
return next(self._iter)
|
||||
|
||||
next = __next__
|
||||
|
||||
|
||||
def guarded_iter(*args):
|
||||
if len(args) == 1:
|
||||
i = args[0]
|
||||
# Don't double-wrap
|
||||
if isinstance(i, SafeIter):
|
||||
return i
|
||||
if not isinstance(i, range):
|
||||
return SafeIter(i)
|
||||
# Other call styles / targets don't need to be guarded
|
||||
return NullIter(iter(*args))
|
||||
|
||||
|
||||
extra_safe_builtins["iter"] = guarded_iter
|
||||
|
||||
|
||||
def guarded_any(seq):
|
||||
return any(guarded_iter(seq))
|
||||
|
||||
|
||||
extra_safe_builtins["any"] = guarded_any
|
||||
|
||||
|
||||
def guarded_all(seq):
|
||||
return all(guarded_iter(seq))
|
||||
|
||||
|
||||
extra_safe_builtins["all"] = guarded_all
|
||||
|
||||
valid_inplace_types = (list, set)
|
||||
|
||||
inplace_slots = {
|
||||
"+=": "__iadd__",
|
||||
"-=": "__isub__",
|
||||
"*=": "__imul__",
|
||||
"/=": (1 / 2 == 0) and "__idiv__" or "__itruediv__",
|
||||
"//=": "__ifloordiv__",
|
||||
"%=": "__imod__",
|
||||
"**=": "__ipow__",
|
||||
"<<=": "__ilshift__",
|
||||
">>=": "__irshift__",
|
||||
"&=": "__iand__",
|
||||
"^=": "__ixor__",
|
||||
"|=": "__ior__",
|
||||
}
|
||||
|
||||
|
||||
def __iadd__(x, y):
|
||||
x += y
|
||||
return x
|
||||
|
||||
|
||||
def __isub__(x, y):
|
||||
x -= y
|
||||
return x
|
||||
|
||||
|
||||
def __imul__(x, y):
|
||||
x *= y
|
||||
return x
|
||||
|
||||
|
||||
def __idiv__(x, y):
|
||||
x /= y
|
||||
return x
|
||||
|
||||
|
||||
def __ifloordiv__(x, y):
|
||||
x //= y
|
||||
return x
|
||||
|
||||
|
||||
def __imod__(x, y):
|
||||
x %= y
|
||||
return x
|
||||
|
||||
|
||||
def __ipow__(x, y):
|
||||
x **= y
|
||||
return x
|
||||
|
||||
|
||||
def __ilshift__(x, y):
|
||||
x <<= y
|
||||
return x
|
||||
|
||||
|
||||
def __irshift__(x, y):
|
||||
x >>= y
|
||||
return x
|
||||
|
||||
|
||||
def __iand__(x, y):
|
||||
x &= y
|
||||
return x
|
||||
|
||||
|
||||
def __ixor__(x, y):
|
||||
x ^= y
|
||||
return x
|
||||
|
||||
|
||||
def __ior__(x, y):
|
||||
x |= y
|
||||
return x
|
||||
|
||||
|
||||
inplace_ops = {
|
||||
"+=": __iadd__,
|
||||
"-=": __isub__,
|
||||
"*=": __imul__,
|
||||
"/=": __idiv__,
|
||||
"//=": __ifloordiv__,
|
||||
"%=": __imod__,
|
||||
"**=": __ipow__,
|
||||
"<<=": __ilshift__,
|
||||
">>=": __irshift__,
|
||||
"&=": __iand__,
|
||||
"^=": __ixor__,
|
||||
"|=": __ior__,
|
||||
}
|
||||
|
||||
|
||||
def protected_inplacevar(op, var, expr):
|
||||
"""Do an inplace operation
|
||||
|
||||
If the var has an inplace slot, then disallow the operation
|
||||
unless the var an instance of ``valid_inplace_types``.
|
||||
"""
|
||||
if hasattr(var, inplace_slots[op]) and not isinstance(var, valid_inplace_types):
|
||||
try:
|
||||
cls = var.__class__
|
||||
except AttributeError:
|
||||
cls = type(var)
|
||||
raise TypeError(
|
||||
"Augmented assignment to %s objects is not allowed"
|
||||
" in untrusted code" % cls.__name__
|
||||
)
|
||||
return inplace_ops[op](var, expr)
|
||||
|
||||
|
||||
extra_safe_builtins["_inplacevar_"] = protected_inplacevar
|
25
src/karkas_core/karkas_core/singleton.py
Normal file
25
src/karkas_core/karkas_core/singleton.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
|
||||
from karkas_core.modules_system import ModulesManager
|
||||
|
||||
|
||||
class SingletonMeta(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
instance = super().__call__(*args, **kwargs)
|
||||
cls._instances[cls] = instance
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class Singleton(metaclass=SingletonMeta):
|
||||
bot: Bot
|
||||
dp: Dispatcher = None
|
||||
modules_manager: ModulesManager = None
|
||||
storage = {
|
||||
"_fsm_storage": MemoryStorage(),
|
||||
"_routers": [],
|
||||
"_outer_message_middlewares": [],
|
||||
}
|
2140
src/karkas_core/poetry.lock
generated
Normal file
2140
src/karkas_core/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
src/karkas_core/poetry.toml
Normal file
2
src/karkas_core/poetry.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[virtualenvs]
|
||||
in-project = true
|
26
src/karkas_core/pyproject.toml
Normal file
26
src/karkas_core/pyproject.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[tool.poetry]
|
||||
name = "karkas-core"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Максим Слипенко <maxim@slipenko.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "~3.12"
|
||||
aiogram = "^3.10.0"
|
||||
setuptools = "^71.0.1"
|
||||
restrictedpython = "^7.1"
|
||||
semver = "^3.0.2"
|
||||
dataclasses-json = "^0.6.7"
|
||||
fastapi = { version = "^0.111.1", optional = true }
|
||||
hypercorn = { version = "^0.17.3", optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
karkas-blocks = { path = "../karkas_blocks", develop = true }
|
||||
|
||||
[tool.poetry.extras]
|
||||
webhook = ["fastapi", "hypercorn"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
Reference in New Issue
Block a user