0
0
mirror of https://gitflic.ru/project/maks1ms/ocab.git synced 2025-10-11 06:22:37 +03:00
This commit is contained in:
2024-07-09 23:57:48 +03:00
parent 2a2b9e15e8
commit f9f6eaad0d
40 changed files with 693 additions and 183 deletions

View File

@@ -1,6 +1,7 @@
import logging
import os
import time
import traceback
def setup_logger():
@@ -26,4 +27,12 @@ async def log(message):
Она асинхронная, хотя таковой на самом деле не является.
"""
if isinstance(message, Exception):
error_message = f"Error: {str(message)}\n{traceback.format_exc()}"
logging.error(error_message)
else:
logging.info(message)
def log_new(message):
logging.info(message)

View File

@@ -1,11 +1,23 @@
import asyncio
import traceback
from aiogram import Bot, Dispatcher
from routers import include_routers
from src.core.logger import log, setup_logger
from src.core.logger import setup_logger
from src.core.modules_system import ModulesManager
from src.core.modules_system.loaders import FSLoader
from src.core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader
from src.core.singleton import Singleton
from src.modules.standard.config.config import get_telegram_token
from src.modules.standard.database.db_api import connect_database, create_tables
from src.service import paths
bot_modules = [
UnsafeFSLoader(f"{paths.modules_standard}/config"),
UnsafeFSLoader(f"{paths.modules_standard}/database"),
UnsafeFSLoader(f"{paths.modules_standard}/roles"),
FSLoader(f"{paths.modules_standard}/info"),
]
async def main():
@@ -13,17 +25,23 @@ async def main():
database = None
setup_logger()
app = Singleton()
try:
bot = Bot(token=get_telegram_token())
database, path = connect_database()
database.connect()
create_tables(database)
dp = Dispatcher()
await include_routers(dp)
await dp.start_polling(bot)
except Exception as e:
log(e)
app.dp = Dispatcher()
app.modules_manager = ModulesManager()
for module_loader in bot_modules:
app.modules_manager.load(module_loader)
await app.dp.start_polling(bot)
except Exception:
traceback.print_exc()
finally:
if bot is not None:
await bot.session.close()

View File

@@ -0,0 +1 @@
from .modules_manager import ModulesManager

View File

@@ -0,0 +1 @@
from .fs_loader import FSLoader

View File

@@ -0,0 +1,22 @@
import types
from dataclasses import dataclass
from dataclasses_json import dataclass_json
@dataclass_json
@dataclass
class ModuleInfo:
id: str
name: str
description: str
version: str
author: str
class AbstractLoader:
def info(self) -> ModuleInfo:
raise NotImplementedError
def load(self) -> types.ModuleType:
raise NotImplementedError

View File

@@ -0,0 +1,77 @@
import types
from pathlib import Path
from RestrictedPython import compile_restricted_exec
from src.core.modules_system.loaders.fs_loader.policy import (
ALLOWED_IMPORTS,
BUILTINS,
RestrictedPythonPolicy,
)
from src.core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader
class FSLoader(UnsafeFSLoader):
def __init__(self, path):
super().__init__(path)
self.builtins = BUILTINS.copy()
self.builtins["__import__"] = self._hook_import
def load(self):
# TODO handle dependencies from info
self.info()
# with open(os.path.join(self.path, "__init__.py"), "r") as f:
# source = f.read()
return self._hook_import(".")
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 in ALLOWED_IMPORTS:
return ALLOWED_IMPORTS[name]
# TODO: allow only public api for modules
if name.startswith("src."):
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,
}
)
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

View File

@@ -0,0 +1 @@
from .FSLoader import FSLoader

View File

@@ -0,0 +1,101 @@
import typing
import warnings
from _ast import AnnAssign
from typing import Any
import aiogram
from RestrictedPython import (
RestrictingNodeTransformer,
limited_builtins,
safe_builtins,
utility_builtins,
)
from RestrictedPython.Eval import default_guarded_getitem
from RestrictedPython.Guards import full_write_guard
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": typing,
"aiogram": aiogram,
"warnings": warnings,
}
BUILTINS = safe_builtins.copy()
BUILTINS.update(utility_builtins)
BUILTINS.update(limited_builtins)
BUILTINS["__metaclass__"] = _metaclass
BUILTINS["_getitem_"] = default_guarded_getitem
BUILTINS["_write_"] = full_write_guard
class GuardedDictType:
def __call__(self, *args, **kwargs):
return dict(*args, **kwargs)
@staticmethod
def fromkeys(iterable, value=None):
return dict.fromkeys(iterable, value)
BUILTINS["dict"] = GuardedDictType()

View File

@@ -0,0 +1,64 @@
import importlib.util
import os
import sys
from pathlib import Path
from src.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))
print(full_path.parent.absolute())
print(module_name)
# Загружаем спецификацию модуля
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

View File

@@ -0,0 +1 @@
from .UnsafeFSLoader import UnsafeFSLoader

View File

@@ -0,0 +1,29 @@
from src.core.modules_system.loaders.base import AbstractLoader
class ModulesManager:
def __init__(self):
self.modules = {}
def load(self, loader: AbstractLoader):
info = loader.info()
module = loader.load()
print(module)
self.modules[info.id] = {
"info": info,
"module": module,
}
def get_by_id(self, module_id: str):
if module_id not in self.modules:
raise Exception(f"Module with id {module_id} not loaded")
return self.modules[module_id]["module"]
def get_info_by_id(self, module_id: str):
if module_id not in self.modules:
raise Exception(f"Module with id {module_id} not loaded")
return self.modules[module_id]["info"]

View File

@@ -0,0 +1 @@
from .public_api import get_module, register_router

View File

@@ -0,0 +1,13 @@
from aiogram import Router
from src.core.singleton import Singleton
def register_router(router: Router):
app = Singleton()
app.dp.include_router(router)
def get_module(module_id: str):
app = Singleton()
return app.modules_manager.get_by_id(module_id)

View File

@@ -1,7 +1,8 @@
from aiogram import Dispatcher
from src.modules.standard.admin.routers import router as admin_router
from src.modules.standard.info.routers import router as info_router
# from src.modules.standard.info.routers import router as info_router
from src.modules.standard.message_processing.message_api import (
router as process_message,
)
@@ -12,6 +13,6 @@ async def include_routers(dp: Dispatcher):
Подключение роутеров в бота
dp.include_router()
"""
dp.include_router(info_router)
# dp.include_router(info_router)
dp.include_router(admin_router)
dp.include_router(process_message)

18
src/core/singleton.py Normal file
View File

@@ -0,0 +1,18 @@
from aiogram import Dispatcher
from src.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):
dp: Dispatcher = None
modules_manager: ModulesManager = None

View File

@@ -1 +0,0 @@
from . import config, database, exceptions, roles

View File

@@ -1 +1 @@
from . import config
from .config import config

View File

@@ -2,7 +2,7 @@
import yaml
from ....service import paths
from src.service import paths
def get_config(is_test: bool = False) -> dict:

View File

@@ -1,6 +1,9 @@
{
"id": "standard.config",
"name": "Config YAML",
"description": "Модуль для работы с конфигурационным файлом бота (YAML)",
"author": "OCAB Team",
"version": "1.0"
"version": "1.0",
"privileged": true,
"dependencies": {}
}

View File

@@ -1,8 +1,9 @@
import peewee as pw
from aiogram.types import Message
from ....service import paths
from ..exceptions.module_exceptions import MissingModuleName, NotExpectedModuleName
from src.service import paths
from .exceptions import MissingModuleName, NotExpectedModuleName
from .models.chat_stats import ChatStats
from .models.chats import Chats
from .models.messages import Messages
@@ -20,6 +21,7 @@ def connect_database(is_test: bool = False, module: str | None = None):
raise NotExpectedModuleName()
db_path = f"{paths.core}/database"
# WTF?????
_database = pw.SqliteDatabase(f"{db_path}/OCAB.db")
Chats._meta.database = _database
Messages._meta.database = _database

View File

@@ -1,6 +1,9 @@
{
"id": "standard.database",
"name": "Database",
"description": "Модуль для работы с БД",
"author": "OCAB Team",
"version": "1.0"
"version": "1.0",
"privileged": true,
"dependencies": {}
}

View File

@@ -1 +0,0 @@
from . import module_exceptions

View File

@@ -1,6 +0,0 @@
{
"name": "Exceptions",
"description": "Модуль с исключениями",
"author": "OCAB Team",
"version": "1.0"
}

View File

@@ -0,0 +1,12 @@
from aiogram import F, Router
from src.core.modules_system.public_api import register_router
from .handlers import get_chat_info, get_user_info
router = Router()
router.message.register(get_user_info, F.text.startswith("/info"))
router.message.register(get_chat_info, F.text.startswith("/chatinfo"))
register_router(router)

View File

@@ -1,12 +1,26 @@
# flake8: noqa
from typing import Type
from aiogram import Bot
from aiogram.types import Message
from src.core.logger import log
from src.modules.standard.config.config import get_user_role_name
from src.modules.standard.database.db_api import *
from src.modules.standard.roles.roles import Roles
from src.core.modules_system.public_api import get_module
from src.modules.standard.database.db_api import (
Message,
get_chat_all_stat,
get_message_ai_model,
get_user,
get_user_all_stats,
get_user_id,
get_user_name,
get_user_rep,
get_user_role,
get_user_tag,
)
from .interfaces import IRoles
Roles: Type[IRoles] = get_module("standard.roles").Roles
async def get_info_answer_by_id(message: Message, bot: Bot, user_id: int):

View File

@@ -0,0 +1,11 @@
{
"id": "standard.info",
"name": "Info",
"description": "Модуль с информацией",
"author": "OCAB Team",
"version": "1.0",
"privileged": false,
"dependencies": {
"standard.roles": "^1.0.0"
}
}

View File

@@ -0,0 +1,20 @@
class IRoles:
user: str
moderator: str
admin: str
bot: str
def __init__(self):
pass
async def check_admin_permission(self, user_id: int) -> bool:
pass
async def check_moderator_permission(self, user_id) -> bool:
pass
async def get_role_name(self, role_id) -> str:
pass
async def get_user_permission(self, user_id) -> str | None:
pass

View File

@@ -1,10 +0,0 @@
# flake8: noqa
from aiogram import F, Router
from src.modules.standard.info.handlers import get_chat_info, get_user_info
router = Router()
router.message.register(get_user_info, F.text.startswith("/info") == True)
router.message.register(get_chat_info, F.text.startswith("/chatinfo") == True)

View File

@@ -0,0 +1 @@
from .roles import Roles

View File

@@ -1,6 +1,12 @@
{
"id": "standard.roles",
"name": "Roles",
"description": "Модуль для работы с ролями",
"author": "OCAB Team",
"version": "1.0"
"version": "1.0",
"privileged": true,
"dependencies": {
"standard.config": "^1.0.0",
"standard.database": "^1.0.0"
}
}

View File

@@ -1,7 +1,7 @@
from ..config.config import get_config
from ..database.db_api import get_user_role
from src.core.modules_system.public_api import get_module
yaml_load = get_config()
get_user_role = get_module("standard.database").db_api.get_user_role
config = get_module("standard.config").config
class Roles:
@@ -9,7 +9,7 @@ class Roles:
moderator = "MODERATOR"
admin = "ADMIN"
bot = "BOT"
__roles = yaml_load["ROLES"]
__roles = config["ROLES"]
def __init__(self):
self.user_role_id = self.__roles[self.user]