Merged with feat/config-module

This commit is contained in:
2024-08-02 16:59:51 +03:00
parent eecc59ca94
commit e2f5e8daba
158 changed files with 8194 additions and 1512 deletions
-2
View File
@@ -1,2 +0,0 @@
import ocab_core
import service
+23
View File
@@ -0,0 +1,23 @@
FROM python:3.12-slim as builder
RUN pip install poetry
RUN mkdir -p /app
COPY . /app
# Фикс
RUN sed -i '/ocab-core = {/{s/, develop = true//}' /app/src/gnomik/pyproject.toml && \
sed -i '/ocab-modules = {/{s/, develop = true//}' /app/src/gnomik/pyproject.toml && \
sed -i '/ocab-core = {/{s/, develop = true//}' /app/src/ocab_modules/pyproject.toml
WORKDIR /app/src/gnomik
RUN poetry lock && poetry install
FROM python:3.12-slim as base
COPY --from=builder /app/src/gnomik /app
WORKDIR /app
ENV PATH="/app/.venv/bin:$PATH"
CMD ["python", "-m", "gnomik"]
+14
View File
@@ -0,0 +1,14 @@
**/Dockerfile
**/*.dockerignore
**/docker-compose.yml
**/.git
**/.gitignore
**/.venv
**/.mypy_cache
**/__pycache__/
src/gnomik/config.yaml
src/gnomik/database/*
+55
View File
@@ -0,0 +1,55 @@
# Gnomик
![Логотип](./docs/gnomik.jpg)
Чат-бот помощник в [ALT Gnome Chat](https://t.me/alt_gnome_chat).
ALT Regular Gnome Community - открытое сообщество пользователей операционной системы ALT Regular Gnome.
- [Канал](https://t.me/alt_gnome)
- [Wiki](https://alt-gnome.wiki)
## Описание
Gnomик - это чат-бот, разработанный на платформе Open Chat AI Bot (OCAB) для Telegram. Он предоставляет различные функции и возможности, помогающие пользователям операционной системы ALT Regular Gnome.
## Функционал
<!--
TODO: описать функционал
-->
## Запуск
### Docker
1. Соберите Docker-образ:
```bash
docker build -t gnomik .
```
2. Запустите контейнер:
```bash
docker run -p 9000:9000 -v ./config.yaml:/app/config.yaml -v ./database:/app/database gnomik
```
Замените `./config.yaml` и `./database` на пути к вашим локальным файлам конфигурации и паки для базы данных.
### Вручную
1. Активируйте виртуальное окружение Gnomика:
```bash
poetry shell
```
2. Запустите бота:
```bash
python -m gnomik
```
## Конфигурация
Конфигурация бота находится в файле `config.yaml`.
## Модули
Список загружаемых модулей указан в файле `__main__.py`.
+18
View File
@@ -0,0 +1,18 @@
core:
mode: WEBHOOK
token: xxx
webhook:
public_url: xxx
filters:
approved_chat_id: -4128011756 | -4128011756
default_chat_tag: '@alt_gnome_chat'
miniapp:
public_url: xxx
yandexgpt:
catalogid: xxx
inword: помогите | не работает
prompt: Ты чат-бот ...
startword: Бот| Бот, | бот | бот,
token: xxx
token_for_answer: 2000
token_for_request: 8000
+12
View File
@@ -0,0 +1,12 @@
version: '3'
services:
app:
build:
context: ../..
dockerfile: src/gnomik/Dockerfile
ports:
- 9000:9000
volumes:
- ./config.yaml:/app/config.yaml
- ./database:/app/database
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

+29
View File
@@ -0,0 +1,29 @@
import asyncio
from ocab_core import OCAB
from ocab_modules import module_loader
async def main():
ocab = OCAB()
await ocab.init_app(
[
module_loader("standard", "config", safe=False),
module_loader("standard", "database", safe=False),
module_loader("standard", "fsm_database_storage", safe=False),
module_loader("standard", "roles", safe=False),
module_loader("external", "yandexgpt", safe=False),
#
module_loader("standard", "command_helper"),
module_loader("standard", "info"),
module_loader("standard", "filters"),
module_loader("external", "create_report_apps"),
module_loader("standard", "admin"),
module_loader("standard", "message_processing"),
module_loader("standard", "miniapp", safe=False),
]
)
await ocab.start()
asyncio.run(main())
+2162
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
[virtualenvs]
in-project = true
+15
View File
@@ -0,0 +1,15 @@
[tool.poetry]
name = "gnomik"
version = "0.1.0"
description = ""
authors = ["Максим Слипенко <maxim@slipenko.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "~3.12"
ocab-core = { extras=["webhook"], path = "../ocab_core", develop = true }
ocab-modules = { path = "../ocab_modules", develop = true }
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
+23
View File
@@ -0,0 +1,23 @@
# OCAB Core
Это ядро OCAB, содержащее базовые компоненты:
- Система управления модулями.
- Логирование.
- Утилиты.
## Система управления модулями
Система управления модулями отвечает за:
- Загрузку модулей.
- Проверку зависимостей.
- Предоставление API для взаимодействия между модулями.
## Логирование
Модуль логирования предоставляет функции для записи логов в консоль.
## Утилиты
Модуль утилит содержит вспомогательные функции, например, для форматирования текста.
-21
View File
@@ -1,21 +0,0 @@
TELEGRAM:
TOKEN: xxxxxxxxxxxxxxxxxxxxxxxxx
APPROVED_CHAT_ID: "-123456789 | -012345678"
ADMINCHATID: -12345678
DEFAULT_CHAT_TAG: "@alt_gnome_chat"
CHECK_BOT: True
YANDEXGPT:
TOKEN: xxxxxxxxxxxxxxxxxxxxxxxxx
TOKEN_FOR_REQUEST: 8000
TOKEN_FOR_ANSWER: 2000
CATALOGID: xxxxxxxxxxxxxxxxxxxxxxxxx
PROMPT: "Ты чат-бот ..."
STARTWORD: "Бот| Бот, | бот | бот,"
INWORD: "помогите | не работает"
ROLES:
ADMIN: 2
MODERATOR: 1
USER: 0
BOT: 3
-29
View File
@@ -1,29 +0,0 @@
import logging
import os
import time
import traceback
def setup_logger():
"""
Настройка логирования
"""
current_date = time.strftime("%d-%m-%Y")
log_dir = os.path.join(os.path.dirname(__file__), "log")
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f"log-{current_date}.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(message)s",
datefmt="%H:%M:%S",
)
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)
-61
View File
@@ -1,61 +0,0 @@
import asyncio
import traceback
from aiogram import Bot, Dispatcher
from ocab_core.logger import log, 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}/fsm_database_storage"),
UnsafeFSLoader(f"{paths.modules_standard}/roles"),
UnsafeFSLoader(f"{paths.modules_external}/yandexgpt"),
FSLoader(f"{paths.modules_standard}/command_helper"),
FSLoader(f"{paths.modules_standard}/info"),
FSLoader(f"{paths.modules_standard}/filters"),
FSLoader(f"{paths.modules_external}/create_report_apps"),
FSLoader(f"{paths.modules_standard}/admin"),
FSLoader(f"{paths.modules_standard}/message_processing"),
]
async def main():
bot = None
setup_logger()
app = Singleton()
try:
app.bot = Bot(token=get_telegram_token())
app.modules_manager = ModulesManager()
for module_loader in bot_modules:
info = module_loader.info()
log(f"Loading {info.name}({info.id}) module")
await app.modules_manager.load(module_loader)
app.dp = Dispatcher(storage=app.storage["_fsm_storage"])
app.dp.include_routers(*app.storage["_routers"])
for middleware in app.storage["_outer_message_middlewares"]:
app.dp.message.outer_middleware.register(middleware)
await app.modules_manager.late_init()
await app.dp.start_polling(app.bot)
except Exception:
traceback.print_exc()
finally:
if bot is not None:
await app.bot.session.close()
if __name__ == "__main__":
asyncio.run(main())
@@ -1 +0,0 @@
from .fs_loader import FSLoader
@@ -1,24 +0,0 @@
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 | list[str]
privileged: bool
dependencies: dict
class AbstractLoader:
def info(self) -> ModuleInfo:
raise NotImplementedError
def load(self) -> types.ModuleType:
raise NotImplementedError
@@ -1,85 +0,0 @@
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 = []
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
# Check dependencies
for dependency, version in info.dependencies.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}, but it is not loaded"
)
loaded_dependency_info = loaded_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.append(
{
"info": info,
"module": module,
}
)
if hasattr(module, "module_init"):
await module.module_init()
async def late_init(self):
for m in self.modules:
module = m["module"]
if hasattr(module, "module_late_init"):
await 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"]
+1
View File
@@ -0,0 +1 @@
from .main import OCAB
+38
View 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
View File
@@ -0,0 +1,46 @@
import logging
import traceback
app_logger = logging.getLogger("ocab")
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
+118
View File
@@ -0,0 +1,118 @@
import traceback
from typing import TYPE_CHECKING
from aiogram import Bot, Dispatcher
from ocab_core.lib import register_bot_webhook
from ocab_core.logger import CustomLogger, log, setup_logger
from ocab_core.modules_system import ModulesManager
from ocab_core.modules_system.public_api import get_module
from ocab_core.singleton import Singleton
if TYPE_CHECKING:
from ocab_modules.standard.config import IConfig
class OCAB:
def __init__(self) -> None:
pass
async def init_app(self, bot_modules):
setup_logger()
singleton = Singleton()
try:
singleton.modules_manager = ModulesManager()
for module_loader in bot_modules:
info = module_loader.info()
log(f"Loading {info.name} ({info.id}) module")
await singleton.modules_manager.load(module_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,
)
@@ -0,0 +1,2 @@
from .fs_loader import FSLoader
from .unsafe_fs_loader import UnsafeFSLoader
@@ -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
@@ -3,6 +3,7 @@ from pathlib import Path
from RestrictedPython import compile_restricted_exec
# from ocab_core.logger import log
from ocab_core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader
from ocab_core.modules_system.safe.policy import (
ALLOWED_IMPORTS,
@@ -16,13 +17,30 @@ class FSLoader(UnsafeFSLoader):
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):
info = self.info()
if info.privileged:
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)
@@ -44,12 +62,14 @@ class FSLoader(UnsafeFSLoader):
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 = __import__(name, *args, **kwargs)
module.__ocab_module_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)
@@ -58,9 +78,7 @@ class FSLoader(UnsafeFSLoader):
module = types.ModuleType(name)
module.__dict__.update(
{
"__builtins__": self.builtins,
}
{"__builtins__": self.builtins, "__ocab_module_id__": self.module_id}
)
result = compile_restricted_exec(src, "<string>", policy=RestrictedPythonPolicy)
@@ -0,0 +1,169 @@
import importlib
import inspect
import pkg_resources
import semver
from ocab_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"]
@@ -1,10 +1,12 @@
from ocab_core.logger import log
from .public_api import (
Storage,
get_fsm_context,
get_module,
log,
register_outer_message_middleware,
register_router,
set_chat_menu_button,
set_my_commands,
)
from .utils import Utils
@@ -1,3 +1,4 @@
import inspect
import types
from typing import Any, Tuple, Union
@@ -5,10 +6,16 @@ from aiogram import BaseMiddleware, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.storage.base import StorageKey
from ocab_core.logger import log
# from ocab_core.logger import log
from ocab_core.modules_system.loaders.base import DependencyInfo
from ocab_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)
@@ -40,17 +47,47 @@ async def get_fsm_context(chat_id: int, user_id: int) -> FSMContext:
def set_fsm(storage):
app = Singleton()
log(storage)
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 "__ocab_module_id__" in caller_globals:
caller_module_id = caller_globals["__ocab_module_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):
@@ -63,9 +100,16 @@ def get_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:
except AttributeError as e:
if "is not allowed" in str(e):
raise PermissionError(str(e))
results.append(None)
if len(results) == 1:
@@ -1,3 +1,4 @@
import types
from _ast import AnnAssign
from typing import Any
@@ -9,8 +10,8 @@ from RestrictedPython import (
utility_builtins,
)
from RestrictedPython.Eval import default_guarded_getitem, default_guarded_getiter
from RestrictedPython.Guards import (
full_write_guard,
from RestrictedPython.Guards import ( # guarded_setattr,; full_write_guard,
_write_wrapper,
guarded_unpack_sequence,
safer_getattr,
)
@@ -95,6 +96,39 @@ def safes_getattr(object, name, default=None, getattr=safer_getattr):
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)
@@ -103,7 +137,8 @@ BUILTINS["__metaclass__"] = _metaclass
BUILTINS["_getitem_"] = default_guarded_getitem
BUILTINS["_getattr_"] = safes_getattr
BUILTINS["_getiter_"] = default_guarded_getiter
BUILTINS["_write_"] = full_write_guard
BUILTINS["_write_"] = write_guard()
BUILTINS["_unpack_sequence_"] = guarded_unpack_sequence
BUILTINS["staticmethod"] = staticmethod
BUILTINS["tuple"] = tuple
BUILTINS["reversed"] = reversed
+2140
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
[virtualenvs]
in-project = true
+26
View File
@@ -0,0 +1,26 @@
[tool.poetry]
name = "ocab-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]
ocab-modules = { path = "../ocab_modules", develop = true }
[tool.poetry.extras]
webhook = ["fastapi", "hypercorn"]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
+12
View File
@@ -0,0 +1,12 @@
# OCAB Modules
OCAB Modules содержит набор модулей для платформы Open Chat AI Bot (OCAB).
## Описание
OCAB - это платформа для создания чат-ботов Telegram. Модули - это расширения, которые добавляют функциональность ботам OCAB.
## Типы модулей
* **Стандартные модули (standard.*):** Предоставляют основные функции, такие как управление пользователями, ролями и настройками.
* **Дополнительные официальные модули (external.*):** Разработаны командой OCAB и предоставляют расширенные возможности, такие как интеграция с нейросетями, внешними сервисами и API.
-9
View File
@@ -1,9 +0,0 @@
{
"id": "external.yandexgpt",
"name": "Yandex GPT",
"description": "Модуль для работы с Yandex GPT",
"author": "OCAB Team",
"version": "1.0.0",
"privileged": true,
"dependencies": {}
}
@@ -0,0 +1 @@
from .lib import module_loader
@@ -0,0 +1,19 @@
# Модуль Create Report Apps
Модуль `create_report_apps` предназначен для помощи пользователям в создании отчетов об ошибках в приложениях.
## Функциональность
- Задает пользователю ряд вопросов, необходимых для составления отчета.
- Собирает информацию о системе пользователя.
- Формирует отчет в текстовом формате.
## Команды
- `/create_report_apps` - запустить процесс создания отчета.
## Использование
1. Отправьте команду `/create_report_apps` боту в личных сообщениях или в групповом чате.
2. Ответьте на вопросы бота.
3. Бот сформирует отчет и отправит его вам.
@@ -0,0 +1,22 @@
# Модуль YandexGPT
Модуль `yandexgpt` интегрирует в бота OCAB нейросеть YandexGPT.
## Функциональность
- Позволяет боту отвечать на сообщения пользователей, используя YandexGPT.
- Строит линию контекста для нейросети, используя историю сообщений.
## Конфигурация
- `yandexgpt::token` - API-ключ для доступа к YandexGPT.
- `yandexgpt::catalogid` - идентификатор каталога YandexGPT.
- `yandexgpt::prompt` - системная подсказка для YandexGPT.
- `yandexgpt::startword` - слова, с которых должно начинаться сообщение, чтобы бот ответил.
- `yandexgpt::inword` - слова, которые должны быть в сообщении, чтобы бот ответил.
## Использование
1. Настройте конфигурационные параметры модуля.
2. Отправьте боту сообщение, которое соответствует условиям, указанным в параметрах `startword` и `inword`.
3. Бот ответит на сообщение, используя YandexGPT.
@@ -1 +1,2 @@
from .handlers import answer_to_message
from .main import module_init
@@ -1,14 +1,36 @@
# flake8: noqa
from typing import TYPE_CHECKING
from aiogram import Bot
from aiogram.types import Message
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 ocab_core.modules_system.public_api import get_module, log
from .yandexgpt import YandexGPT
if TYPE_CHECKING:
from ocab_modules.standard.config import IConfig
from ocab_modules.standard.database.db_api import add_message as IAddMessage
config: "IConfig" = get_module(
"standard.config",
"config",
)
from ocab_modules.standard.database.db_api import add_message
def get_yandexgpt_catalog_id():
return config.get("yandexgpt::catalogid")
def get_yandexgpt_token():
return config.get("yandexgpt::token")
def get_yandexgpt_prompt():
return config.get("yandexgpt::prompt")
add_message: "IAddMessage" = get_module("standard.database", "db_api.add_message")
async def answer_to_message(message: Message, bot: Bot):
@@ -0,0 +1,21 @@
{
"id": "external.yandexgpt",
"name": "Yandex GPT",
"description": "Модуль для работы с Yandex GPT",
"author": "OCAB Team",
"version": "1.0.0",
"privileged": false,
"dependencies": {
"required": {
"standard.config": "^1.0.0",
"standard.database": "^1.0.0"
}
},
"pythonDependencies": {
"required": {
"aiohttp": "*",
"requests": "*",
"json": "*"
}
}
}
@@ -0,0 +1,50 @@
from typing import TYPE_CHECKING
from ocab_core.modules_system.public_api import get_module
if TYPE_CHECKING:
from ocab_modules.standard.config import IConfig
config: "IConfig" = get_module("standard.config", "config")
def module_init():
config.register(
"yandexgpt::token",
"password",
required=True,
)
config.register(
"yandexgpt::token_for_request",
"int",
default_value=8000,
)
config.register(
"yandexgpt::token_for_answer",
"int",
default_value=2000,
)
config.register(
"yandexgpt::catalogid",
"password",
required=True,
)
config.register(
"yandexgpt::prompt",
"string",
default_value="Ты чат-бот ...",
)
config.register(
"yandexgpt::startword",
"string",
default_value="Бот| Бот, | бот | бот,",
)
config.register(
"yandexgpt::inword",
"string",
default_value="помогите | не работает",
)
@@ -1,7 +1,7 @@
# flake8: noqa
from aiogram import F, Router
from src.ocab_modules.external.yandexgpt.handlers import answer_to_message
from .handlers import answer_to_message
router = Router()
# Если сообщение содержит в начале текст "Гномик" или "гномик" или отвечает на сообщение бота, то вызывается функция answer_to_message
@@ -1,14 +1,31 @@
# flake8: noqa
import asyncio
import json
from typing import TYPE_CHECKING
import aiohttp
import requests
from ocab_core.logger import log
from ocab_core.modules_system.public_api import get_module, log
from ...standard.config.config import *
from ...standard.database import *
if TYPE_CHECKING:
from ocab_modules.standard.config import IConfig
from ocab_modules.standard.database import db_api as IDbApi
db_api: "IDbApi" = get_module("standard.database", "db_api")
config: "IConfig" = get_module("standard.config", "config")
def get_yandexgpt_token_for_answer():
return config.get("yandexgpt::token_for_answer")
def get_yandexgpt_token_for_request():
return config.get("yandexgpt::token_for_request")
def get_yandexgpt_prompt():
return config.get("yandexgpt::prompt")
class YandexGPT:
@@ -108,8 +125,11 @@ class YandexGPT:
input_messages,
stream=False,
temperature=0.6,
max_tokens=get_yandexgpt_token_for_request(),
max_tokens=None,
):
if max_tokens is None:
max_tokens = get_yandexgpt_token_for_request()
url = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"
gpt = f"gpt://{self.catalog_id}/yandexgpt/latest"
headers = {
+25
View File
@@ -0,0 +1,25 @@
import importlib
import os
from ocab_core.modules_system.loaders.fs_loader import FSLoader
from ocab_core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader
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)
ocab_modules_path = get_module_directory("ocab_modules")
def module_loader(namespace: str, module_name: str, safe=True):
if not safe:
return UnsafeFSLoader(f"{ocab_modules_path}/{namespace}/{module_name}")
else:
return FSLoader(f"{ocab_modules_path}/{namespace}/{module_name}")
@@ -0,0 +1,20 @@
# Модуль Admin
Модуль `admin` предоставляет администраторам и модераторам чата инструменты для управления:
## Функциональность
- Удаление сообщений.
- Получение ID чата.
## Команды
- `/rm` - удалить сообщение, на которое отвечает команда.
- `/chatID` - получить ID текущего чата.
## Использование
1. Ответьте на сообщение, которое нужно удалить.
2. Отправьте команду `/rm`.
Чтобы получить ID чата, отправьте команду `/chatID`.
@@ -1,10 +1,19 @@
# flake8: noqa
from typing import TYPE_CHECKING
from aiogram import Bot
from aiogram.types import Message
from ocab_core.modules_system.public_api import get_module
get_default_chat_tag = get_module("standard.config", "get_default_chat_tag")
if TYPE_CHECKING:
from ocab_modules.standard.config import IConfig
config: "IConfig" = get_module("standard.config", "config")
def get_default_chat_tag():
return config.get("filters::default_chat_tag")
async def delete_message(message: Message, bot: Bot):
@@ -6,6 +6,8 @@
"version": "1.0.0",
"privileged": false,
"dependencies": {
"standard.filters": "^1.0.0"
"required": {
"standard.filters": "^1.0.0"
}
}
}
@@ -0,0 +1,24 @@
# Модуль Command Helper
Модуль `command_helper` упрощает регистрацию команд бота и управление ими.
## Функциональность
- Регистрация команд бота.
- Установка команд для пользователей в зависимости от их роли.
## Использование
1. Импортируйте функцию `register_command`.
2. Вызовите функцию `register_command`, передав ей название команды, ее описание и роль пользователя,
которому доступна эта команда.
## Пример
```python
from ocab_core.modules_system.public_api import get_module
register_command = get_module("standard.command_helper", "register_command")
register_command("my_command", "Описание моей команды", role="ADMIN")
```
@@ -0,0 +1,14 @@
{
"id": "standard.command_helper",
"name": "Command helper",
"description": "Модуль для отображения команд при вводе '/'",
"author": "OCAB Team",
"version": "1.0.0",
"privileged": false,
"dependencies": {
"required": {
"standard.roles": "^1.0.0",
"standard.database": "^1.0.0"
}
}
}
@@ -1,4 +1,4 @@
from typing import Any, Awaitable, Callable, Dict
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict
from aiogram import BaseMiddleware
from aiogram.types import BotCommand, TelegramObject
@@ -9,15 +9,18 @@ from ocab_core.modules_system.public_api import (
set_my_commands,
)
if TYPE_CHECKING:
from ocab_modules.standard.database import db_api as IDbApi
from ocab_modules.standard.roles import Roles as IRoles
commands = dict()
db_api = get_module(
db_api: "IDbApi" = get_module(
"standard.database",
"db_api",
)
Roles = get_module("standard.roles", "Roles")
Roles: "IRoles" = get_module("standard.roles", "Roles")
def register_command(command, description, role="USER"):
@@ -0,0 +1,28 @@
# Модуль Config
Модуль `config` управляет конфигурацией бота.
## Функциональность
- Загрузка конфигурации из файла `config.yaml`.
- Сохранение конфигурации в файл.
- Регистрация параметров конфигурации.
- Получение значений параметров.
## Использование
1. Импортируйте объект `config`.
2. Вызовите метод `register`, чтобы зарегистрировать параметр конфигурации.
3. Вызовите метод `get`, чтобы получить значение параметра.
## Пример
```python
from ocab_core.modules_system.public_api import get_module
config = get_module("standard.config", "config")
config.register("my_parameter", "string", default_value="default")
value = config.get("my_parameter")
```
@@ -0,0 +1,2 @@
from .config import IConfig, config
from .main import module_late_init
@@ -0,0 +1,7 @@
# flake8: noqa
from .config_manager import ConfigManager
IConfig = ConfigManager
config: ConfigManager = ConfigManager(config_path="config.yaml")
@@ -0,0 +1,113 @@
import inspect
from typing import Any, Dict, List
import yaml
class ConfigManager:
def __init__(self, config_path: str):
self.config_path = config_path
self._config: Dict[str, Dict[str, Any]] = {}
self._metadata: Dict[str, Dict[str, Any]] = {}
def load(self, file_path: str = ""):
if not file_path:
file_path = self.config_path
def build_key(prev, next):
if prev:
return f"{prev}::{next}"
return next
def recurse_set(value, key=""):
if isinstance(value, dict):
for k, v in value.items():
recurse_set(v, build_key(key, k))
return
if key in self._metadata:
self._config[key] = value
with open(file_path, "r", encoding="utf-8") as file:
data = yaml.safe_load(file)
recurse_set(data)
def save(self, file_path: str = ""):
if not file_path:
file_path = self.config_path
def nested_dict(flat_dict):
result = {}
for key, value in flat_dict.items():
keys = key.split("::")
d = result
for k in keys[:-1]:
d = d.setdefault(k, {})
d[keys[-1]] = value
return result
with open(file_path, "w", encoding="utf-8") as file:
yaml.dump(nested_dict(self._config), file, allow_unicode=True)
def _check_rights(self, key, module_id, access_type="get"):
return
def get(self, key: str):
module_id = self._get_module_id()
self._check_rights(key, module_id)
return self._config.get(key, self._metadata.get(key).get("default_value"))
def get_meta(self, key: str):
module_id = self._get_module_id()
self._check_rights(key, module_id, "get_meta")
return self._metadata.get(key)
def _get_module_id(self):
caller_frame = inspect.currentframe().f_back.f_back
caller_globals = caller_frame.f_globals
module_id = caller_globals.get("__ocab_module_id__")
return module_id
def mass_set(self, updates: Dict[str, Any]):
module_id = self._get_module_id()
for key, value in updates.items():
self._check_rights(key, module_id, "set")
if key in self._metadata:
# TODO: add checks to validate the type and value based on metadata
self._config[key] = value
else:
raise KeyError(f"Key {key} is not registered.")
def register(
self,
key: str,
value_type: str,
options: List[Any] = None,
default_value=None,
editable: bool = True,
shared: bool = False,
required: bool = False,
visible: bool = True,
pretty_name: str = "",
description: str = "",
):
module_id = self._get_module_id()
self._check_rights(key, module_id, "register")
if key in self._metadata:
raise ValueError("ERROR")
self._metadata[key] = {
"type": value_type,
"options": options,
"default_value": default_value,
"visible": visible,
"editable": editable,
"shared": shared,
"required": required,
"pretty_name": pretty_name,
"description": description,
"module_id": module_id,
}
@@ -5,5 +5,14 @@
"author": "OCAB Team",
"version": "1.0.0",
"privileged": true,
"dependencies": {}
"dependencies": {
"optional": {
"standard.miniapp": "^1.0.0"
}
},
"pythonDependencies": {
"optional": {
"flet": "^0.23.2"
}
}
}
@@ -0,0 +1,30 @@
from ocab_core.modules_system.public_api import get_module, log
from .config import config
from .miniapp_ui import get_miniapp_blueprint
def register_settings_page():
try:
register_page = get_module("standard.miniapp", "register_page")
prefix = "settings"
register_page(
name="Настройки",
path="/settings",
blueprint=get_miniapp_blueprint(config, prefix),
prefix=prefix,
role="ADMIN",
)
pass
except Exception as e:
log(str(e))
pass
def module_late_init():
register_settings_page()
pass
@@ -0,0 +1,266 @@
import asyncio
from typing import TYPE_CHECKING
from .config_manager import ConfigManager
try:
import dash_bootstrap_components as dbc
import flask
from dash_extensions.enrich import ALL, Input, Output, State, dcc, html
DASH_AVAILABLE = True
except ImportError:
DASH_AVAILABLE = False
from ocab_core.modules_system.public_api import get_module
if TYPE_CHECKING:
from ocab_modules.standard.roles import Roles as IRoles
def create_control(key: str, config: ConfigManager):
value = config.get(key)
meta = config.get_meta(key)
component_id = {
"type": "setting",
"key": key,
}
label_text = meta.get("pretty_name") or key
if meta.get("type") in ["string", "int", "float", "password"]:
input_type = {
"string": "text",
"int": "number",
"float": "number",
"password": "password",
}.get(meta.get("type"), "text")
input_props = {
"id": component_id,
"type": input_type,
}
if meta.get("type") != "password":
input_props["value"] = value
if meta.get("type") == "int":
input_props["step"] = 1
input_props["pattern"] = r"\d+"
elif meta.get("type") == "float":
input_props["step"] = "any"
component = dbc.Input(**input_props, invalid=False)
elif meta.get("type") == "select":
options = [{"label": opt, "value": opt} for opt in meta.get("options", [])]
component = dcc.Dropdown(
id=component_id,
options=options,
value=value,
style={
"padding-left": 0,
"padding-right": 0,
},
)
elif meta.get("type") == "checkbox":
component = dbc.Checkbox(
id=component_id,
checked=value,
label=label_text,
)
else:
return None
row = []
if meta.get("type") != "checkbox":
row.append(dbc.Label(label_text))
row.append(component)
if meta.get("description"):
row.append(dbc.FormText(meta.get("description")))
return dbc.Row(row, className="mb-3 mx-1")
def build_settings_tree(config: ConfigManager):
tree = {}
for key, value in config._metadata.items():
if not value["visible"]:
continue
parts = key.split("::")
control = create_control(key, config)
current = tree
for i, part in enumerate(parts[:-1]):
if part not in current:
current[part] = {"__controls": []}
current = current[part]
current["__controls"].append(control)
return tree
def create_card(category, controls):
return dbc.Card(
[
dbc.CardHeader(html.H3(category, className="mb-0")),
dbc.CardBody(controls),
],
className="mb-3",
)
def create_settings_components(tree, level=0):
components = []
for category, subtree in tree.items():
if category == "__controls":
continue
controls = subtree.get("__controls", [])
subcomponents = create_settings_components(subtree, level + 1)
if controls or subcomponents:
card_content = controls + subcomponents
card = create_card(category, card_content)
components.append(card)
return components
def get_miniapp_blueprint(config: ConfigManager, prefix: str):
Roles: "type[IRoles]" = get_module("standard.roles", "Roles")
roles = Roles()
import datetime
from dash_extensions.enrich import DashBlueprint
bp = DashBlueprint()
def create_layout():
settings_tree = build_settings_tree(config)
settings_components = create_settings_components(settings_tree)
layout = html.Div(
[
html.Script(),
html.H1("Настройки"),
dbc.Form(settings_components),
html.Div(id="save-confirmation"),
dbc.Button(
"Сохранить",
id="save-settings",
color="primary",
className="mt-3 w-100",
n_clicks=0,
),
html.Div(id="settings-update-trigger", style={"display": "none"}),
dcc.Store(id="settings-store"),
],
style={
"padding": "20px",
},
)
return layout
bp.layout = create_layout
@bp.callback(
Output("save-confirmation", "children"),
Output("settings-store", "data"),
Input("save-settings", "n_clicks"),
State({"type": "setting", "key": ALL}, "value"),
State({"type": "setting", "key": ALL}, "id"),
prevent_initial_call=True,
allow_duplicate=True,
)
def save_settings(n_clicks, values, keys):
if n_clicks > 0:
user = getattr(flask.g, "user", None)
if user is None:
return (
dbc.Alert(
"Вы не авторизованы!",
color="danger",
duration=10000,
),
"-",
)
if not asyncio.run(roles.check_admin_permission(user["id"])):
return (
dbc.Alert(
"Вы не администратор!",
color="danger",
duration=10000,
),
"-",
)
# TODO: добавить валидацию значений
updated_settings = {}
for value, id_dict in zip(values, keys):
key: str = id_dict["key"]
if prefix:
key = key.removeprefix(f"{prefix}-")
meta = config.get_meta(key)
if meta["type"] == "password":
if value: # Only update if a new value is provided
updated_settings[key] = value
else:
updated_settings[key] = value
config.mass_set(updated_settings)
config.save()
now = datetime.datetime.now()
date_str = now.strftime("%H:%M:%S")
return (
dbc.Alert(
f"Настройки сохранены в {date_str}",
color="success",
duration=10000,
),
date_str,
)
bp.clientside_callback(
"""
function(n_clicks) {
const buttonSelector = '#%s-save-settings';
if (n_clicks > 0) {
document.querySelector(buttonSelector).disabled = true;
}
}
"""
% (prefix),
Input("save-settings", "n_clicks"),
)
bp.clientside_callback(
"""
function(data) {
const buttonSelector = '#%s-save-settings';
if (data) {
document.querySelector(buttonSelector).disabled = false;
}
}
"""
% (prefix),
Input("settings-store", "data"),
)
return bp
@@ -1,9 +1,7 @@
import peewee as pw
from aiogram.types import Message
from src.service import paths
from .exceptions import MissingModuleName, NotExpectedModuleName
from .exceptions import NotExpectedModuleName
from .models.chat_stats import ChatStats
from .models.chats import Chats
from .models.db import database_proxy
@@ -14,14 +12,9 @@ from .models.users import Users
def connect_database(is_test: bool = False, module: str | None = None):
if is_test:
if not module:
raise MissingModuleName()
db_path = f"{paths.modules_standard}/{module}/tests/database"
else:
if module:
raise NotExpectedModuleName()
db_path = f"{paths.core}/database"
if module:
raise NotExpectedModuleName()
db_path = "database"
database = pw.SqliteDatabase(f"{db_path}/OCAB.db")
database_proxy.initialize(database)
@@ -158,7 +151,7 @@ def get_user_name(user_id):
return user.user_name if user else None
def get_user_role(user_id):
def get_user_role(user_id: str):
user = Users.get_or_none(Users.id == user_id)
return user.user_role if user else None

Some files were not shown because too many files have changed in this diff Show More