mirror of
https://gitflic.ru/project/alt-gnome/karkas.git
synced 2025-11-25 00:58:05 +03:00
Merged with private/new-module-system
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
import src.core
|
||||
import src.service
|
||||
import ocab_core
|
||||
import service
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from routers import include_routers
|
||||
|
||||
from src.core.logger import log, setup_logger
|
||||
from src.modules.standard.config.config import get_telegram_token
|
||||
from src.modules.standard.database.db_api import connect_database, create_tables
|
||||
|
||||
|
||||
async def main():
|
||||
bot = None
|
||||
database = None
|
||||
setup_logger()
|
||||
|
||||
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)
|
||||
finally:
|
||||
if bot is not None:
|
||||
await bot.session.close()
|
||||
if database is not None:
|
||||
database.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1 +0,0 @@
|
||||
from . import config, database, exceptions, roles
|
||||
@@ -1 +0,0 @@
|
||||
from . import config
|
||||
@@ -1 +0,0 @@
|
||||
from . import db_api, models
|
||||
@@ -1 +0,0 @@
|
||||
from . import module_exceptions
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "Exceptions",
|
||||
"description": "Модуль с исключениями",
|
||||
"author": "OCAB Team",
|
||||
"version": "1.0"
|
||||
}
|
||||
@@ -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)
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "Roles",
|
||||
"description": "Модуль для работы с ролями",
|
||||
"author": "OCAB Team",
|
||||
"version": "1.0"
|
||||
}
|
||||
@@ -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)
|
||||
49
src/ocab_core/main.py
Normal file
49
src/ocab_core/main.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import asyncio
|
||||
import traceback
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
|
||||
from ocab_core.logger import setup_logger
|
||||
from ocab_core.modules_system import ModulesManager
|
||||
from ocab_core.modules_system.loaders import FSLoader
|
||||
from ocab_core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader
|
||||
from ocab_core.singleton import Singleton
|
||||
from ocab_modules.standard.config.config import get_telegram_token
|
||||
from 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():
|
||||
bot = None
|
||||
database = None
|
||||
setup_logger()
|
||||
|
||||
app = Singleton()
|
||||
|
||||
try:
|
||||
bot = Bot(token=get_telegram_token())
|
||||
|
||||
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()
|
||||
if database is not None:
|
||||
database.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
1
src/ocab_core/modules_system/__init__.py
Normal file
1
src/ocab_core/modules_system/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .modules_manager import ModulesManager
|
||||
1
src/ocab_core/modules_system/loaders/__init__.py
Normal file
1
src/ocab_core/modules_system/loaders/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .fs_loader import FSLoader
|
||||
24
src/ocab_core/modules_system/loaders/base.py
Normal file
24
src/ocab_core/modules_system/loaders/base.py
Normal file
@@ -0,0 +1,24 @@
|
||||
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
|
||||
privileged: bool
|
||||
dependencies: dict
|
||||
|
||||
|
||||
class AbstractLoader:
|
||||
def info(self) -> ModuleInfo:
|
||||
raise NotImplementedError
|
||||
|
||||
def load(self) -> types.ModuleType:
|
||||
raise NotImplementedError
|
||||
73
src/ocab_core/modules_system/loaders/fs_loader/FSLoader.py
Normal file
73
src/ocab_core/modules_system/loaders/fs_loader/FSLoader.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
from RestrictedPython import compile_restricted_exec
|
||||
|
||||
from ocab_core.modules_system.loaders.fs_loader.policy import (
|
||||
ALLOWED_IMPORTS,
|
||||
BUILTINS,
|
||||
RestrictedPythonPolicy,
|
||||
)
|
||||
from ocab_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):
|
||||
info = self.info()
|
||||
if info.privileged:
|
||||
raise Exception("Only non privileged modules are allowed to be imported")
|
||||
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):
|
||||
for allowed in ALLOWED_IMPORTS:
|
||||
if name == allowed or name.startswith(f"{allowed}."):
|
||||
return __import__(name, *args, **kwargs)
|
||||
|
||||
if name == "ocab_core.modules_system.public_api":
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
from .FSLoader import FSLoader
|
||||
98
src/ocab_core/modules_system/loaders/fs_loader/policy.py
Normal file
98
src/ocab_core/modules_system/loaders/fs_loader/policy.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from _ast import AnnAssign
|
||||
from typing import Any
|
||||
|
||||
from RestrictedPython import (
|
||||
RestrictingNodeTransformer,
|
||||
limited_builtins,
|
||||
safe_builtins,
|
||||
utility_builtins,
|
||||
)
|
||||
from RestrictedPython.Eval import default_guarded_getitem
|
||||
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
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
|
||||
BUILTINS["staticmethod"] = staticmethod
|
||||
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,64 @@
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from ocab_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
|
||||
@@ -0,0 +1 @@
|
||||
from .UnsafeFSLoader import UnsafeFSLoader
|
||||
66
src/ocab_core/modules_system/modules_manager.py
Normal file
66
src/ocab_core/modules_system/modules_manager.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import semver
|
||||
|
||||
from ocab_core.modules_system.loaders.base import AbstractLoader
|
||||
|
||||
|
||||
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 not semver.Version.parse(version).match(r):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ModulesManager:
|
||||
def __init__(self):
|
||||
self.modules = {}
|
||||
|
||||
def load(self, loader: AbstractLoader):
|
||||
info = loader.info()
|
||||
|
||||
if info.id in self.modules:
|
||||
return
|
||||
|
||||
for dependency, version in info.dependencies.items():
|
||||
if dependency not in self.modules:
|
||||
raise Exception(
|
||||
f"Module {info.id} depends on {dependency}, but it is not loaded"
|
||||
)
|
||||
loaded_dependency_info = self.modules[dependency]["info"]
|
||||
if not is_version_compatible(loaded_dependency_info.version, version):
|
||||
raise Exception(
|
||||
f"Module {info.id} depends on {dependency}, "
|
||||
f"but version {version} is not compatible"
|
||||
)
|
||||
|
||||
module = loader.load()
|
||||
|
||||
self.modules[info.id] = {
|
||||
"info": info,
|
||||
"module": module,
|
||||
}
|
||||
|
||||
if hasattr(module, "module_init"):
|
||||
module.module_init()
|
||||
|
||||
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"]
|
||||
3
src/ocab_core/modules_system/public_api/__init__.py
Normal file
3
src/ocab_core/modules_system/public_api/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from ocab_core.logger import log # noqa
|
||||
|
||||
from .public_api import Storage, get_module, register_router
|
||||
54
src/ocab_core/modules_system/public_api/public_api.py
Normal file
54
src/ocab_core/modules_system/public_api/public_api.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import types
|
||||
from typing import Any, Tuple, Union
|
||||
|
||||
from aiogram import Router
|
||||
|
||||
from ocab_core.singleton import Singleton
|
||||
|
||||
|
||||
def register_router(router: Router):
|
||||
app = Singleton()
|
||||
app.dp.include_router(router)
|
||||
|
||||
|
||||
def get_module(
|
||||
module_id: str, paths=None
|
||||
) -> Union[types.ModuleType, Union[Any, None], Tuple[Union[Any, None], ...]]:
|
||||
app = Singleton()
|
||||
module = app.modules_manager.get_by_id(module_id)
|
||||
|
||||
if paths is None:
|
||||
return module
|
||||
|
||||
if isinstance(paths, str):
|
||||
paths = [paths]
|
||||
|
||||
results = []
|
||||
|
||||
for path in paths:
|
||||
current_obj = module
|
||||
try:
|
||||
parts = path.split(".")
|
||||
for part in parts:
|
||||
current_obj = getattr(current_obj, part)
|
||||
results.append(current_obj)
|
||||
except AttributeError:
|
||||
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)
|
||||
@@ -1,8 +1,9 @@
|
||||
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.message_processing.message_api import (
|
||||
from src.ocab_modules.standard.admin.routers import router as admin_router
|
||||
|
||||
# from src.modules.standard.info.routers import router as info_router
|
||||
from src.ocab_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)
|
||||
19
src/ocab_core/singleton.py
Normal file
19
src/ocab_core/singleton.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from aiogram import Dispatcher
|
||||
|
||||
from ocab_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
|
||||
storage = dict()
|
||||
@@ -4,14 +4,13 @@ import asyncio
|
||||
from aiogram import Bot
|
||||
from aiogram.types import Message
|
||||
|
||||
from src.core.logger import log
|
||||
from src.modules.external.yandexgpt.yandexgpt import *
|
||||
from src.modules.standard.config.config import (
|
||||
from ocab_modules.external.yandexgpt.yandexgpt import *
|
||||
from ocab_modules.standard.config.config import (
|
||||
get_yandexgpt_catalog_id,
|
||||
get_yandexgpt_prompt,
|
||||
get_yandexgpt_token,
|
||||
)
|
||||
from src.modules.standard.database.db_api import add_message
|
||||
from ocab_modules.standard.database.db_api import add_message
|
||||
|
||||
|
||||
async def answer_to_message(message: Message, bot: Bot):
|
||||
@@ -1,7 +1,7 @@
|
||||
# flake8: noqa
|
||||
from aiogram import F, Router
|
||||
|
||||
from src.modules.external.yandexgpt.handlers import answer_to_message
|
||||
from src.ocab_modules.external.yandexgpt.handlers import answer_to_message
|
||||
|
||||
router = Router()
|
||||
# Если сообщение содержит в начале текст "Гномик" или "гномик" или отвечает на сообщение бота, то вызывается функция answer_to_message
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
import aiohttp
|
||||
import requests
|
||||
|
||||
from src.core.logger import log
|
||||
from ocab_core.logger import log
|
||||
|
||||
from ...standard.config.config import *
|
||||
from ...standard.database import *
|
||||
@@ -4,7 +4,7 @@ import time
|
||||
from aiogram import Bot
|
||||
from aiogram.types import Message
|
||||
|
||||
from src.modules.standard.config.config import get_default_chat_tag
|
||||
from src.ocab_modules.standard.config.config import get_default_chat_tag
|
||||
|
||||
|
||||
async def delete_message(message: Message, bot: Bot):
|
||||
@@ -1,13 +1,13 @@
|
||||
# flake8: noqa
|
||||
from aiogram import F, Router
|
||||
|
||||
from src.modules.standard.admin.handlers import (
|
||||
from src.ocab_modules.standard.admin.handlers import (
|
||||
chat_not_in_approve_list,
|
||||
delete_message,
|
||||
error_access,
|
||||
get_chat_id,
|
||||
)
|
||||
from src.modules.standard.filters.filters import (
|
||||
from src.ocab_modules.standard.filters.filters import (
|
||||
ChatModerOrAdminFilter,
|
||||
ChatNotInApproveFilter,
|
||||
)
|
||||
1
src/ocab_modules/standard/config/__init__.py
Normal file
1
src/ocab_modules/standard/config/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .config import config
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import yaml
|
||||
|
||||
from ....service import paths
|
||||
from src.service import paths
|
||||
|
||||
|
||||
def get_config(is_test: bool = False) -> dict:
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "standard.config",
|
||||
"name": "Config YAML",
|
||||
"description": "Модуль для работы с конфигурационным файлом бота (YAML)",
|
||||
"author": "OCAB Team",
|
||||
"version": "1.0"
|
||||
"version": "1.0.0",
|
||||
"privileged": true,
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from src.modules.standard.config.config import get_config
|
||||
from src.ocab_modules.standard.config.config import get_config
|
||||
|
||||
yaml_load = get_config(is_test=True)
|
||||
|
||||
5
src/ocab_modules/standard/database/__init__.py
Normal file
5
src/ocab_modules/standard/database/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from . import db_api, models
|
||||
|
||||
|
||||
def module_init():
|
||||
db_api.connect_database()
|
||||
@@ -1,10 +1,12 @@
|
||||
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.db import database_proxy
|
||||
from .models.messages import Messages
|
||||
from .models.user_stats import UserStats
|
||||
from .models.users import Users
|
||||
@@ -20,14 +22,12 @@ def connect_database(is_test: bool = False, module: str | None = None):
|
||||
raise NotExpectedModuleName()
|
||||
db_path = f"{paths.core}/database"
|
||||
|
||||
_database = pw.SqliteDatabase(f"{db_path}/OCAB.db")
|
||||
Chats._meta.database = _database
|
||||
Messages._meta.database = _database
|
||||
Users._meta.database = _database
|
||||
UserStats._meta.database = _database
|
||||
ChatStats._meta.database = _database
|
||||
database = pw.SqliteDatabase(f"{db_path}/OCAB.db")
|
||||
database_proxy.initialize(database)
|
||||
database.connect()
|
||||
create_tables(database)
|
||||
|
||||
return _database, f"{db_path}/OCAB.db"
|
||||
return database, f"{db_path}/OCAB.db"
|
||||
|
||||
|
||||
def create_tables(db: pw.SqliteDatabase):
|
||||
@@ -138,7 +138,7 @@ def get_chat_all_stat(chat_id):
|
||||
# Работа с таблицей пользователей
|
||||
|
||||
|
||||
def get_user(user_id):
|
||||
def get_user(user_id) -> Users | None:
|
||||
return Users.get_or_none(Users.id == user_id)
|
||||
|
||||
|
||||
@@ -196,7 +196,8 @@ def change_user_role(user_id, new_user_role):
|
||||
|
||||
def get_message(message_chat_id, message_id):
|
||||
return Messages.get_or_none(
|
||||
Messages.message_chat_id == message_chat_id, Messages.message_id == message_id
|
||||
Messages.message_chat_id == message_chat_id,
|
||||
Messages.message_id == message_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "standard.database",
|
||||
"name": "Database",
|
||||
"description": "Модуль для работы с БД",
|
||||
"author": "OCAB Team",
|
||||
"version": "1.0"
|
||||
"version": "1.0.0",
|
||||
"privileged": true,
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import peewee as pw
|
||||
|
||||
from .db import database_proxy
|
||||
|
||||
|
||||
class ChatStats(pw.Model):
|
||||
class Meta: ...
|
||||
class Meta:
|
||||
database = database_proxy
|
||||
|
||||
chat_id = pw.IntegerField(null=False)
|
||||
date = pw.DateField(null=False)
|
||||
@@ -1,8 +1,11 @@
|
||||
import peewee as pw
|
||||
|
||||
from .db import database_proxy
|
||||
|
||||
|
||||
class Chats(pw.Model):
|
||||
class Meta: ...
|
||||
class Meta:
|
||||
database = database_proxy
|
||||
|
||||
chat_name = pw.CharField(null=False)
|
||||
chat_type = pw.IntegerField(null=False, default=10)
|
||||
3
src/ocab_modules/standard/database/models/db.py
Normal file
3
src/ocab_modules/standard/database/models/db.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from peewee import DatabaseProxy
|
||||
|
||||
database_proxy = DatabaseProxy()
|
||||
@@ -1,8 +1,11 @@
|
||||
import peewee as pw
|
||||
|
||||
from .db import database_proxy
|
||||
|
||||
|
||||
class Messages(pw.Model):
|
||||
class Meta: ...
|
||||
class Meta:
|
||||
database = database_proxy
|
||||
|
||||
message_chat_id = pw.IntegerField(null=False)
|
||||
message_id = pw.IntegerField(null=False)
|
||||
@@ -1,8 +1,11 @@
|
||||
import peewee as pw
|
||||
|
||||
from .db import database_proxy
|
||||
|
||||
|
||||
class UserStats(pw.Model):
|
||||
class Meta: ...
|
||||
class Meta:
|
||||
database = database_proxy
|
||||
|
||||
chat_id = pw.IntegerField(null=False)
|
||||
user_id = pw.IntegerField(null=False)
|
||||
@@ -1,8 +1,11 @@
|
||||
import peewee as pw
|
||||
|
||||
from .db import database_proxy
|
||||
|
||||
|
||||
class Users(pw.Model):
|
||||
class Meta: ...
|
||||
class Meta:
|
||||
database = database_proxy
|
||||
|
||||
user_tag = pw.CharField(null=True)
|
||||
user_name = pw.CharField(null=False) # до 255 символов
|
||||
@@ -2,9 +2,9 @@ from aiogram import Bot
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
from src.core.logger import log
|
||||
from src.modules.standard.config.config import get_aproved_chat_id
|
||||
from src.modules.standard.roles.roles import Roles
|
||||
from ocab_core.logger import log
|
||||
from ocab_modules.standard.config.config import get_aproved_chat_id
|
||||
from ocab_modules.standard.roles.roles import Roles
|
||||
|
||||
|
||||
class ChatModerOrAdminFilter(BaseFilter):
|
||||
14
src/ocab_modules/standard/info/__init__.py
Normal file
14
src/ocab_modules/standard/info/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from aiogram import F, Router
|
||||
|
||||
from ocab_core.modules_system.public_api import register_router
|
||||
|
||||
from .handlers import get_chat_info, get_user_info
|
||||
|
||||
|
||||
def module_init():
|
||||
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)
|
||||
@@ -1,46 +1,59 @@
|
||||
# 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 ocab_core.modules_system.public_api import get_module, log
|
||||
|
||||
from .interfaces import IDbApi, IRoles
|
||||
|
||||
db_api: Type[IDbApi] = get_module(
|
||||
"standard.database",
|
||||
"db_api",
|
||||
)
|
||||
|
||||
Roles: Type[IRoles] = get_module("standard.roles", "Roles")
|
||||
|
||||
|
||||
async def get_info_answer_by_id(message: Message, bot: Bot, user_id: int):
|
||||
if get_message_ai_model(message.chat.id, message.message_id) is not None:
|
||||
ai_model = db_api.get_message_ai_model(message.chat.id, message.message_id)
|
||||
if ai_model is not None:
|
||||
await message.reply(
|
||||
"Это сообщение было сгенерировано ботом используя модель: "
|
||||
+ get_message_ai_model(message.chat.id, message.message_id)
|
||||
"Это сообщение было сгенерировано ботом используя модель: " + ai_model
|
||||
)
|
||||
elif user_id == bot.id:
|
||||
return
|
||||
|
||||
if user_id == bot.id:
|
||||
await message.reply("Это сообщение было отправлено ботом")
|
||||
elif get_user(user_id) is None:
|
||||
return
|
||||
|
||||
user = db_api.get_user(user_id)
|
||||
|
||||
if user is None:
|
||||
await message.reply("Пользователь не найден")
|
||||
# print(get_user(user_id))
|
||||
await log(f"Пользователь не найден: {user_id}, {get_user(user_id)}")
|
||||
else:
|
||||
roles = Roles()
|
||||
answer = (
|
||||
f"Пользователь: {get_user_name(user_id)}\n"
|
||||
f"Роль: {await roles.get_role_name(role_id=get_user_role(user_id))}\n"
|
||||
f"Тег: @{get_user_tag(user_id)}\n"
|
||||
f"Кол-во сообщений: {get_user_all_stats(user_id)}\n"
|
||||
f"Репутация: {get_user_rep(user_id)}"
|
||||
)
|
||||
await message.reply(answer)
|
||||
await log(f"Пользователь не найден: {user_id}, {user}")
|
||||
return
|
||||
|
||||
roles = Roles()
|
||||
answer = (
|
||||
f"Пользователь: {user.user_name}\n"
|
||||
f"Роль: {await roles.get_role_name(role_id=user.user_role)}\n"
|
||||
f"Тег: @{user.user_tag}\n"
|
||||
f"Кол-во сообщений: {user.user_stats}\n"
|
||||
f"Репутация: {user.user_rep}"
|
||||
)
|
||||
await message.reply(answer)
|
||||
|
||||
|
||||
async def get_user_info(message: Message, bot: Bot):
|
||||
# Проверяем содержимое сообщения, если содержит вторым элементом тег пользователя, то выводим информацию о нем
|
||||
# Если сообщение отвечает на другое сообщение, то выводим информацию о пользователе, на чье сообщение был ответ
|
||||
# Если это бот то выводим информацию что это бот и какая модель yandexgpt используется
|
||||
# Если это бот то выводим информацию, что это бот и какая модель yandexgpt используется
|
||||
try:
|
||||
if len(message.text.split()) > 1 and message.text.split()[1].startswith("@"):
|
||||
user_tag = message.text.split()[1][1:]
|
||||
user_id = get_user_id(user_tag)
|
||||
user_id = db_api.get_user_id(user_tag)
|
||||
if user_id:
|
||||
await get_info_answer_by_id(message, bot, user_id)
|
||||
else:
|
||||
@@ -63,7 +76,7 @@ async def get_chat_info(message: Message, bot: Bot):
|
||||
answer = (
|
||||
f"*Название чата:* {message.chat.title}\n"
|
||||
f"*ID чата:* `{message.chat.id}`\n \n"
|
||||
f"*Суммарное количество сообщений в чате:* {get_chat_all_stat(message.chat.id)}\n"
|
||||
f"*Суммарное количество сообщений в чате:* {db_api.get_chat_all_stat(message.chat.id)}\n"
|
||||
f"*Количество пользователей в чате:* {await bot.get_chat_member_count(message.chat.id)}\n"
|
||||
f"*Количество администраторов в чате:* {len(await bot.get_chat_administrators(message.chat.id))}"
|
||||
)
|
||||
12
src/ocab_modules/standard/info/info.json
Normal file
12
src/ocab_modules/standard/info/info.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "standard.info",
|
||||
"name": "Info",
|
||||
"description": "Модуль с информацией",
|
||||
"author": "OCAB Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": false,
|
||||
"dependencies": {
|
||||
"standard.roles": "^1.0.0",
|
||||
"standard.database": "^1.0.0"
|
||||
}
|
||||
}
|
||||
47
src/ocab_modules/standard/info/interfaces.py
Normal file
47
src/ocab_modules/standard/info/interfaces.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class IUsers:
|
||||
user_id: int
|
||||
user_name: str
|
||||
user_role: int
|
||||
user_tag: str
|
||||
user_stats: int
|
||||
user_rep: int
|
||||
|
||||
|
||||
class IDbApi:
|
||||
|
||||
@staticmethod
|
||||
def get_message_ai_model(message_chat_id: Any, message_id: Any) -> Any | None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_user(user_id: int) -> IUsers | None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_chat_all_stat(chat_id: int) -> int:
|
||||
pass
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
from aiogram import Bot, F, Router, types
|
||||
|
||||
from src.core.logger import log
|
||||
from src.modules.external.yandexgpt.handlers import answer_to_message
|
||||
from src.modules.standard.config.config import (
|
||||
from ocab_core.logger import log
|
||||
from ocab_modules.external.yandexgpt.handlers import answer_to_message
|
||||
from ocab_modules.standard.config.config import (
|
||||
get_aproved_chat_id,
|
||||
get_yandexgpt_in_words,
|
||||
get_yandexgpt_start_words,
|
||||
)
|
||||
from src.modules.standard.database.db_api import *
|
||||
from ocab_modules.standard.database.db_api import *
|
||||
|
||||
|
||||
async def chat_check(message: types.Message):
|
||||
1
src/ocab_modules/standard/roles/__init__.py
Normal file
1
src/ocab_modules/standard/roles/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .roles import Roles
|
||||
12
src/ocab_modules/standard/roles/info.json
Normal file
12
src/ocab_modules/standard/roles/info.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "standard.roles",
|
||||
"name": "Roles",
|
||||
"description": "Модуль для работы с ролями",
|
||||
"author": "OCAB Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": true,
|
||||
"dependencies": {
|
||||
"standard.config": "^1.0.0",
|
||||
"standard.database": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
from ..config.config import get_config
|
||||
from ..database.db_api import get_user_role
|
||||
from ocab_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: dict = 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]
|
||||
@@ -9,10 +9,14 @@ from aiogram.types import Message
|
||||
from aiogram.types import inline_keyboard_button as types
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from src.modules.standard.config.config import get_telegram_check_bot
|
||||
from src.modules.standard.database.db_api import *
|
||||
from src.modules.standard.moderation.moderation import ban_user, mute_user, unmute_user
|
||||
from src.modules.standard.roles.roles import Roles
|
||||
from src.ocab_modules.standard.config.config import get_telegram_check_bot
|
||||
from src.ocab_modules.standard.database.db_api import *
|
||||
from src.ocab_modules.standard.moderation.moderation import (
|
||||
ban_user,
|
||||
mute_user,
|
||||
unmute_user,
|
||||
)
|
||||
from src.ocab_modules.standard.roles.roles import Roles
|
||||
|
||||
|
||||
async def create_math_task():
|
||||
@@ -1,6 +1,6 @@
|
||||
from aiogram import F, Router
|
||||
|
||||
from src.modules.standard.welcome.handlers import check_new_user
|
||||
from src.ocab_modules.standard.welcome.handlers import check_new_user
|
||||
|
||||
router = Router()
|
||||
|
||||
Reference in New Issue
Block a user