mirror of
https://gitflic.ru/project/alt-gnome/karkas.git
synced 2025-10-06 21:06:06 +03:00
Merged with feat/config-module
This commit is contained in:
12
src/ocab_modules/README.md
Normal file
12
src/ocab_modules/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# OCAB Modules
|
||||
|
||||
OCAB Modules содержит набор модулей для платформы Open Chat AI Bot (OCAB).
|
||||
|
||||
## Описание
|
||||
|
||||
OCAB - это платформа для создания чат-ботов Telegram. Модули - это расширения, которые добавляют функциональность ботам OCAB.
|
||||
|
||||
## Типы модулей
|
||||
|
||||
* **Стандартные модули (standard.*):** Предоставляют основные функции, такие как управление пользователями, ролями и настройками.
|
||||
* **Дополнительные официальные модули (external.*):** Разработаны командой OCAB и предоставляют расширенные возможности, такие как интеграция с нейросетями, внешними сервисами и API.
|
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "external.yandexgpt",
|
||||
"name": "Yandex GPT",
|
||||
"description": "Модуль для работы с Yandex GPT",
|
||||
"author": "OCAB Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": true,
|
||||
"dependencies": {}
|
||||
}
|
1
src/ocab_modules/ocab_modules/__init__.py
Normal file
1
src/ocab_modules/ocab_modules/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .lib import module_loader
|
19
src/ocab_modules/ocab_modules/external/create_report_apps/README.md
vendored
Normal file
19
src/ocab_modules/ocab_modules/external/create_report_apps/README.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Модуль Create Report Apps
|
||||
|
||||
Модуль `create_report_apps` предназначен для помощи пользователям в создании отчетов об ошибках в приложениях.
|
||||
|
||||
## Функциональность
|
||||
|
||||
- Задает пользователю ряд вопросов, необходимых для составления отчета.
|
||||
- Собирает информацию о системе пользователя.
|
||||
- Формирует отчет в текстовом формате.
|
||||
|
||||
## Команды
|
||||
|
||||
- `/create_report_apps` - запустить процесс создания отчета.
|
||||
|
||||
## Использование
|
||||
|
||||
1. Отправьте команду `/create_report_apps` боту в личных сообщениях или в групповом чате.
|
||||
2. Ответьте на вопросы бота.
|
||||
3. Бот сформирует отчет и отправит его вам.
|
22
src/ocab_modules/ocab_modules/external/yandexgpt/README.md
vendored
Normal file
22
src/ocab_modules/ocab_modules/external/yandexgpt/README.md
vendored
Normal file
@@ -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):
|
21
src/ocab_modules/ocab_modules/external/yandexgpt/info.json
vendored
Normal file
21
src/ocab_modules/ocab_modules/external/yandexgpt/info.json
vendored
Normal file
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
}
|
50
src/ocab_modules/ocab_modules/external/yandexgpt/main.py
vendored
Normal file
50
src/ocab_modules/ocab_modules/external/yandexgpt/main.py
vendored
Normal file
@@ -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
src/ocab_modules/ocab_modules/lib.py
Normal file
25
src/ocab_modules/ocab_modules/lib.py
Normal 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}")
|
20
src/ocab_modules/ocab_modules/standard/admin/README.md
Normal file
20
src/ocab_modules/ocab_modules/standard/admin/README.md
Normal file
@@ -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"):
|
28
src/ocab_modules/ocab_modules/standard/config/README.md
Normal file
28
src/ocab_modules/ocab_modules/standard/config/README.md
Normal file
@@ -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
|
7
src/ocab_modules/ocab_modules/standard/config/config.py
Normal file
7
src/ocab_modules/ocab_modules/standard/config/config.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
from .config_manager import ConfigManager
|
||||
|
||||
IConfig = ConfigManager
|
||||
|
||||
config: ConfigManager = ConfigManager(config_path="config.yaml")
|
113
src/ocab_modules/ocab_modules/standard/config/config_manager.py
Normal file
113
src/ocab_modules/ocab_modules/standard/config/config_manager.py
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
30
src/ocab_modules/ocab_modules/standard/config/main.py
Normal file
30
src/ocab_modules/ocab_modules/standard/config/main.py
Normal file
@@ -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
|
266
src/ocab_modules/ocab_modules/standard/config/miniapp_ui.py
Normal file
266
src/ocab_modules/ocab_modules/standard/config/miniapp_ui.py
Normal file
@@ -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
|
||||
|
29
src/ocab_modules/ocab_modules/standard/filters/README.md
Normal file
29
src/ocab_modules/ocab_modules/standard/filters/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Модуль Filters
|
||||
|
||||
Модуль `filters` предоставляет фильтры для aiogram, которые используются для ограничения доступа к командам
|
||||
и обработчикам событий.
|
||||
|
||||
## Фильтры
|
||||
|
||||
- `ChatModerOrAdminFilter` - пропускает сообщения только от модераторов и администраторов чата.
|
||||
- `ChatNotInApproveFilter` - пропускает сообщения только из чатов, не входящих в список разрешенных.
|
||||
|
||||
## Использование
|
||||
|
||||
Фильтры можно использовать в декораторах `@router.message` и `@router.callback_query`.
|
||||
|
||||
## Пример
|
||||
|
||||
```python
|
||||
from aiogram import Router
|
||||
from ocab_core.modules_system.public_api import get_module
|
||||
|
||||
ChatModerOrAdminFilter = get_module("standard.filters", "ChatModerOrAdminFilter")
|
||||
|
||||
router = Router()
|
||||
|
||||
@router.message(ChatModerOrAdminFilter())
|
||||
async def admin_command(message: Message):
|
||||
# Обработка команды, доступной только администраторам и модераторам.
|
||||
pass
|
||||
```
|
@@ -0,0 +1,6 @@
|
||||
from .filters import (
|
||||
ChatModerOrAdminFilter,
|
||||
ChatNotInApproveFilter,
|
||||
chat_not_in_approve,
|
||||
module_init,
|
||||
)
|
52
src/ocab_modules/ocab_modules/standard/filters/filters.py
Normal file
52
src/ocab_modules/ocab_modules/standard/filters/filters.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
from ocab_core.modules_system.public_api import get_module, log
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ocab_modules.standard.config import IConfig
|
||||
|
||||
config: "IConfig" = get_module("standard.config", "config")
|
||||
Roles = get_module("standard.roles", "Roles")
|
||||
|
||||
|
||||
def module_init():
|
||||
config.register("filters::approved_chat_id", "string", shared=True)
|
||||
config.register("filters::default_chat_tag", "string", shared=True)
|
||||
|
||||
|
||||
def get_approved_chat_id() -> list:
|
||||
# Возваращем сплитованный список id чатов в формате int
|
||||
return [
|
||||
int(chat_id) for chat_id in config.get("filters::approved_chat_id").split(" | ")
|
||||
]
|
||||
|
||||
|
||||
def chat_not_in_approve(message: Message) -> bool:
|
||||
chat_id = message.chat.id
|
||||
if chat_id in get_approved_chat_id():
|
||||
log(f"Chat in approve list: {chat_id}")
|
||||
return False
|
||||
else:
|
||||
log(f"Chat not in approve list: {chat_id}")
|
||||
return True
|
||||
|
||||
|
||||
class ChatModerOrAdminFilter(BaseFilter):
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
user_id = message.from_user.id
|
||||
roles = Roles()
|
||||
admins = await bot.get_chat_administrators(message.chat.id)
|
||||
return (
|
||||
await roles.check_admin_permission(user_id)
|
||||
or await roles.check_moderator_permission(user_id)
|
||||
or any(user_id == admin.user.id for admin in admins)
|
||||
)
|
||||
|
||||
|
||||
class ChatNotInApproveFilter(BaseFilter):
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
return chat_not_in_approve(message)
|
@@ -6,7 +6,9 @@
|
||||
"version": "1.0.0",
|
||||
"privileged": false,
|
||||
"dependencies": {
|
||||
"standard.roles": "^1.0.0",
|
||||
"standard.config": "^1.0.0"
|
||||
"required": {
|
||||
"standard.roles": "^1.0.0",
|
||||
"standard.config": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
# Модуль FSM Database Storage
|
||||
|
||||
Модуль `fsm_database_storage` реализует хранение состояний FSM (Finite State Machine) в базе данных.
|
||||
|
||||
## Функциональность
|
||||
|
||||
- Сохранение состояния FSM в базу данных.
|
||||
- Получение состояния FSM из базы данных.
|
||||
- Обновление данных состояния FSM.
|
||||
|
||||
## Использование
|
||||
|
||||
Модуль автоматически регистрирует хранилище состояний FSM при инициализации.
|
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||
|
||||
from aiogram.fsm.state import State
|
||||
from aiogram.fsm.storage.base import BaseStorage, StorageKey
|
||||
@@ -7,7 +7,14 @@ from aiogram.fsm.storage.base import BaseStorage, StorageKey
|
||||
from ocab_core.modules_system.public_api import get_module, log
|
||||
from ocab_core.modules_system.public_api.public_api import set_fsm
|
||||
|
||||
FSMDataRepository = get_module("standard.database", "repositories.FSMDataRepository")
|
||||
if TYPE_CHECKING:
|
||||
from ocab_modules.standard.database.repositories import (
|
||||
FSMDataRepository as IFSMDataRepository,
|
||||
)
|
||||
|
||||
FSMDataRepository: "type[IFSMDataRepository]" = get_module(
|
||||
"standard.database", "repositories.FSMDataRepository"
|
||||
)
|
||||
|
||||
|
||||
def serialize_key(key: StorageKey) -> str:
|
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "standard.fsm_database_storage",
|
||||
"name": "FSM Database Storage",
|
||||
"description": "Очень полезный модуль",
|
||||
"author": "OCAB Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": false,
|
||||
"dependencies": {
|
||||
"required": {
|
||||
"standard.database": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
15
src/ocab_modules/ocab_modules/standard/info/README.md
Normal file
15
src/ocab_modules/ocab_modules/standard/info/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Модуль Info
|
||||
|
||||
Модуль `info` предоставляет информацию о пользователях и чатах.
|
||||
|
||||
## Команды
|
||||
|
||||
- `/info` - получить информацию о пользователе.
|
||||
- `/chatinfo` - получить информацию о чате.
|
||||
|
||||
## Использование
|
||||
|
||||
Чтобы получить информацию о пользователе, отправьте команду `/info`,
|
||||
ответив на сообщение пользователя или указав его тег.
|
||||
|
||||
Чтобы получить информацию о чате, отправьте команду `/chatinfo`.
|
@@ -1,19 +1,22 @@
|
||||
# flake8: noqa
|
||||
from typing import Type
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import Message
|
||||
|
||||
from ocab_core.modules_system.public_api import get_module, log
|
||||
|
||||
from .interfaces import IDbApi, IRoles
|
||||
if TYPE_CHECKING:
|
||||
from ocab_modules.standard.database import db_api as IDbApi
|
||||
from ocab_modules.standard.roles import Roles as IRoles
|
||||
|
||||
db_api: Type[IDbApi] = get_module(
|
||||
db_api: "IDbApi" = get_module(
|
||||
"standard.database",
|
||||
"db_api",
|
||||
)
|
||||
|
||||
Roles: Type[IRoles] = get_module("standard.roles", "Roles")
|
||||
Roles: "type[IRoles]" = get_module("standard.roles", "Roles")
|
||||
|
||||
|
||||
async def get_info_answer_by_id(message: Message, bot: Bot, user_id: int):
|
@@ -6,8 +6,10 @@
|
||||
"version": "1.0.0",
|
||||
"privileged": false,
|
||||
"dependencies": {
|
||||
"standard.roles": "^1.0.0",
|
||||
"standard.database": "^1.0.0",
|
||||
"standard.command_helper": "^1.0.0"
|
||||
"required": {
|
||||
"standard.roles": "^1.0.0",
|
||||
"standard.database": "^1.0.0",
|
||||
"standard.command_helper": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
# Модуль Message Processing
|
||||
|
||||
Модуль `message_processing` обрабатывает все входящие сообщения.
|
||||
|
||||
## Функциональность
|
||||
|
||||
- Проверка чата и пользователя на наличие в базе данных.
|
||||
- Обновление информации о чате и пользователе.
|
||||
- Добавление статистики сообщений.
|
||||
- Передача сообщения модулю `yandexgpt`, если оно соответствует условиям.
|
||||
|
||||
## Использование
|
||||
|
||||
Модуль автоматически обрабатывает все входящие сообщения.
|
@@ -6,8 +6,10 @@
|
||||
"version": "1.0.0",
|
||||
"privileged": false,
|
||||
"dependencies": {
|
||||
"standard.roles": "^1.0.0",
|
||||
"standard.database": "^1.0.0",
|
||||
"standard.command_helper": "^1.0.0"
|
||||
"required": {
|
||||
"standard.roles": "^1.0.0",
|
||||
"standard.database": "^1.0.0",
|
||||
"standard.command_helper": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,15 +1,26 @@
|
||||
# flake8: noqa
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogram import Bot, F, Router, types
|
||||
|
||||
from ocab_core.modules_system.public_api import get_module, log, register_router
|
||||
|
||||
# from ocab_modules.standard.database.db_api import *
|
||||
if TYPE_CHECKING:
|
||||
from ocab_modules.standard.config import IConfig
|
||||
|
||||
(get_approved_chat_id, get_yandexgpt_in_words, get_yandexgpt_start_words) = get_module(
|
||||
"standard.config",
|
||||
["get_approved_chat_id", "get_yandexgpt_in_words", "get_yandexgpt_start_words"],
|
||||
)
|
||||
config: "IConfig" = get_module("standard.config", "config")
|
||||
|
||||
|
||||
def get_yandexgpt_start_words():
|
||||
return config.get("yandexgpt::startword").split(" | ")
|
||||
|
||||
|
||||
def get_yandexgpt_in_words():
|
||||
return config.get("yandexgpt::inword").split(" | ")
|
||||
|
||||
|
||||
chat_not_in_approve = get_module("standard.filters", ["chat_not_in_approve"])
|
||||
|
||||
answer_to_message = get_module("external.yandexgpt", "answer_to_message")
|
||||
|
||||
@@ -48,7 +59,7 @@ async def chat_check(message: types.Message):
|
||||
# Если чата нет в базе данных, то проверяем его в наличии в конфиге и если он там есть то добавляем его в БД
|
||||
# Если чат есть в базе данных, то pass
|
||||
if get_chat(message.chat.id) is None:
|
||||
if message.chat.id in get_approved_chat_id():
|
||||
if not chat_not_in_approve(message):
|
||||
# print(f"Chat in approve list: {message.chat.id} {message.chat.title}")
|
||||
log(f"Chat in approve list: {message.chat.id} {message.chat.title}")
|
||||
add_chat(message.chat.id, message.chat.title)
|
23
src/ocab_modules/ocab_modules/standard/miniapp/README.md
Normal file
23
src/ocab_modules/ocab_modules/standard/miniapp/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Модуль Miniapp
|
||||
|
||||
Модуль `miniapp` реализует веб-интерфейс для бота, доступный через Telegram Mini Apps.
|
||||
|
||||
## Функциональность
|
||||
|
||||
- Регистрация страниц веб-интерфейса.
|
||||
- Авторизация пользователей через Telegram.
|
||||
|
||||
## Использование
|
||||
|
||||
1. Импортируйте функцию `register_page`.
|
||||
2. Вызовите функцию `register_page`, передав ей название страницы, ее путь, blueprint Dash и префикс.
|
||||
|
||||
## Пример
|
||||
|
||||
```python
|
||||
from ocab_core.modules_system.public_api import get_module
|
||||
|
||||
register_page = get_module("standard.miniapp", "register_page")
|
||||
|
||||
register_page("Моя страница", "/my_page", my_blueprint, prefix="my_page")
|
||||
```
|
@@ -0,0 +1,2 @@
|
||||
from .lib import register_page
|
||||
from .main import module_init, module_late_init
|
@@ -0,0 +1,146 @@
|
||||
import flask
|
||||
from aiogram.utils.web_app import safe_parse_webapp_init_data
|
||||
from dash import Dash
|
||||
from dash_extensions.enrich import Input, Output
|
||||
from flask import request
|
||||
|
||||
# TODO: добавить прокидывание BASE_PATH, т.к. это параметр из настроек
|
||||
|
||||
WEBAPP_LOADER_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OCAB</title>
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
<script>
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data.type === 'iframe-url-changed') {
|
||||
history.pushState(
|
||||
null,
|
||||
'',
|
||||
window.BASE_PATH + event.data.pathname.substring(
|
||||
window.INTERNAL_PATH.length
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
window.addEventListener('popstate', function(event) {
|
||||
var iframe = document.getElementById('app-frame');
|
||||
var iframeWindow = iframe.contentWindow;
|
||||
iframeWindow.history.back();
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
#app-frame {
|
||||
display:none;
|
||||
width:100%;
|
||||
height:100vh;
|
||||
border:none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">Loading...</div>
|
||||
<iframe id="app-frame"></iframe>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tg = window.Telegram.WebApp;
|
||||
document.cookie = `tg_init_data=${JSON.stringify(tg.initData)}; path=/`;
|
||||
|
||||
// if (!tg.initData) return;
|
||||
|
||||
const iframe = document.getElementById('app-frame');
|
||||
|
||||
// Константы для путей
|
||||
const BASE_PATH = '/webapp';
|
||||
const INTERNAL_PATH = '/webapp/_internal';
|
||||
|
||||
window.BASE_PATH = BASE_PATH;
|
||||
window.INTERNAL_PATH = INTERNAL_PATH
|
||||
|
||||
// Текущий путь страницы
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
// Формируем новый путь для iframe
|
||||
let iframeSrc = INTERNAL_PATH;
|
||||
|
||||
// Если текущий путь начинается с BASE_PATH, убираем BASE_PATH из текущего пути
|
||||
if (currentPath.startsWith(BASE_PATH)
|
||||
&& currentPath.length > BASE_PATH.length) {
|
||||
iframeSrc += currentPath.substring(BASE_PATH.length);
|
||||
} else if (currentPath !== '/') {
|
||||
iframeSrc += currentPath;
|
||||
}
|
||||
|
||||
iframe.src = iframeSrc;
|
||||
|
||||
iframe.onload = function() {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
iframe.style.display = 'block';
|
||||
};
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def get_auth_server(bot_token: str):
|
||||
server = flask.Flask(__name__)
|
||||
|
||||
@server.route("/<path:rest>")
|
||||
@server.route("/")
|
||||
def webapp_loader(rest=None):
|
||||
return flask.Response(WEBAPP_LOADER_TEMPLATE, mimetype="text/html")
|
||||
|
||||
@server.before_request
|
||||
def add_auth_data():
|
||||
init_data = request.cookies.get("tg_init_data")
|
||||
if init_data:
|
||||
try:
|
||||
data = safe_parse_webapp_init_data(token=bot_token, init_data=init_data)
|
||||
flask.g.user = data.user.model_dump()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return server
|
||||
|
||||
|
||||
def setup_auth_clientcallbacks(app: Dash):
|
||||
app.clientside_callback(
|
||||
"""
|
||||
function(n_intervals) {
|
||||
if (window.webAppData) {
|
||||
return window.webAppData;
|
||||
}
|
||||
|
||||
function receiveMessage(event) {
|
||||
if (event.data.type === 'webAppData') {
|
||||
window.webAppData = event.data.webApp;
|
||||
window.removeEventListener('message', receiveMessage);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', receiveMessage, false);
|
||||
|
||||
return window.dash_clientside.no_update;
|
||||
}
|
||||
""",
|
||||
Output("hidden-div", "children"),
|
||||
Input("interval-component", "n_intervals"),
|
||||
)
|
||||
|
||||
app.clientside_callback(
|
||||
"""
|
||||
function(pathname) {
|
||||
window.parent.postMessage({ type: 'iframe-url-changed', pathname }, '*');
|
||||
}
|
||||
""",
|
||||
Input("url", "pathname"),
|
||||
)
|
23
src/ocab_modules/ocab_modules/standard/miniapp/info.json
Normal file
23
src/ocab_modules/ocab_modules/standard/miniapp/info.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "standard.miniapp",
|
||||
"name": "Miniapp",
|
||||
"description": "Очень полезный модуль",
|
||||
"author": "OCAB Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": false,
|
||||
"dependencies": {
|
||||
"required": {
|
||||
"standard.config": {
|
||||
"version": "^1.0.0",
|
||||
"uses": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"pythonDependencies": {
|
||||
"required": {
|
||||
"dash": "^2.17.1",
|
||||
"dash_extensions": "^1.0.18",
|
||||
"dash_bootstrap_components": "^1.6.0"
|
||||
}
|
||||
}
|
||||
}
|
191
src/ocab_modules/ocab_modules/standard/miniapp/lib.py
Normal file
191
src/ocab_modules/ocab_modules/standard/miniapp/lib.py
Normal file
@@ -0,0 +1,191 @@
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import dash
|
||||
import dash_bootstrap_components as dbc
|
||||
import flask
|
||||
from dash_extensions.enrich import DashBlueprint, DashProxy, Input, Output, dcc, html
|
||||
from dash_extensions.pages import setup_page_components
|
||||
|
||||
from ocab_core.modules_system.public_api import get_module, log
|
||||
|
||||
from .dash_telegram_auth import get_auth_server, setup_auth_clientcallbacks
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ocab_modules.standard.config import IConfig
|
||||
from ocab_modules.standard.roles import Roles as IRoles
|
||||
|
||||
pages = OrderedDict()
|
||||
|
||||
|
||||
def register_page(name, path, blueprint, prefix="", role="USER"):
|
||||
pages[path] = {
|
||||
"name": name,
|
||||
"blueprint": blueprint,
|
||||
"prefix": prefix,
|
||||
"role": role,
|
||||
}
|
||||
|
||||
|
||||
def register_home_page():
|
||||
page = DashBlueprint()
|
||||
page.layout = html.Div([html.H1("Главная")])
|
||||
register_page("Главная", path="/", blueprint=page)
|
||||
|
||||
|
||||
register_home_page()
|
||||
|
||||
config: "IConfig" = get_module("standard.config", "config")
|
||||
Roles: "type[IRoles]" = get_module("standard.roles", "Roles")
|
||||
|
||||
|
||||
def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash:
|
||||
log(requests_pathname_prefix)
|
||||
|
||||
real_prefix = f"{requests_pathname_prefix}_internal/"
|
||||
|
||||
server = get_auth_server(config.get("core::token"))
|
||||
|
||||
app = DashProxy(
|
||||
pages_folder="",
|
||||
use_pages=True,
|
||||
suppress_callback_exceptions=True,
|
||||
external_stylesheets=[
|
||||
dbc.themes.BOOTSTRAP,
|
||||
dbc.icons.BOOTSTRAP,
|
||||
],
|
||||
external_scripts=[
|
||||
#
|
||||
"https://telegram.org/js/telegram-web-app.js"
|
||||
],
|
||||
server=server,
|
||||
requests_pathname_prefix=real_prefix,
|
||||
routes_pathname_prefix="/_internal/",
|
||||
# requests_pathname_prefix=requests_pathname_prefix,
|
||||
meta_tags=[
|
||||
{"name": "viewport", "content": "width=device-width, initial-scale=1"},
|
||||
],
|
||||
)
|
||||
|
||||
# app.enable_dev_tools(
|
||||
# dev_tools_ui=True,
|
||||
# dev_tools_serve_dev_bundles=True,
|
||||
# )
|
||||
|
||||
# Register pages
|
||||
for path, page in pages.items():
|
||||
page["blueprint"].register(app, path, prefix=page["prefix"])
|
||||
|
||||
# Create navbar
|
||||
navbar = dbc.Navbar(
|
||||
dbc.Container(
|
||||
[
|
||||
dbc.Button(
|
||||
html.I(className="bi bi-list"),
|
||||
id="open-offcanvas",
|
||||
color="light",
|
||||
className="me-2",
|
||||
),
|
||||
dbc.NavbarBrand("OCAB"),
|
||||
]
|
||||
),
|
||||
color="primary",
|
||||
dark=True,
|
||||
)
|
||||
|
||||
roles = Roles()
|
||||
|
||||
def create_layout():
|
||||
user = getattr(flask.g, "user", None)
|
||||
|
||||
if not user:
|
||||
return html.Div()
|
||||
|
||||
user_id = user["id"]
|
||||
user_permission = asyncio.run(roles.get_user_permission(user_id)) or "USER"
|
||||
|
||||
available_pages = {
|
||||
path: page
|
||||
for path, page in pages.items()
|
||||
if (isinstance(page["role"], list) and user_permission in page["role"])
|
||||
or page["role"] == user_permission
|
||||
or page["role"] == "USER"
|
||||
}
|
||||
|
||||
# Create sidebar
|
||||
sidebar = dbc.Offcanvas(
|
||||
id="offcanvas",
|
||||
title="Меню",
|
||||
is_open=False,
|
||||
children=[
|
||||
dbc.Nav(
|
||||
[
|
||||
dbc.NavLink(
|
||||
page["name"],
|
||||
href=f"{real_prefix}/{path.lstrip('/')}",
|
||||
id={"type": "nav-link", "index": path},
|
||||
)
|
||||
for path, page in available_pages.items()
|
||||
],
|
||||
vertical=True,
|
||||
pills=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
layout = html.Div(
|
||||
[
|
||||
dcc.Location(id="url", refresh=False),
|
||||
dcc.Interval(
|
||||
id="init-telegram-interval",
|
||||
interval=100,
|
||||
n_intervals=0,
|
||||
max_intervals=1,
|
||||
),
|
||||
navbar,
|
||||
sidebar,
|
||||
dash.page_container,
|
||||
setup_page_components(),
|
||||
]
|
||||
)
|
||||
|
||||
return layout
|
||||
|
||||
app.layout = create_layout
|
||||
|
||||
setup_auth_clientcallbacks(app)
|
||||
|
||||
# Открытие на кнопку меню
|
||||
app.clientside_callback(
|
||||
"""
|
||||
function(n_clicks) {
|
||||
if (n_clicks == null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
""",
|
||||
Output(
|
||||
"offcanvas",
|
||||
"is_open",
|
||||
),
|
||||
Input("open-offcanvas", "n_clicks"),
|
||||
)
|
||||
|
||||
# Закрываем offcanvas при клике на ссылку в меню
|
||||
app.clientside_callback(
|
||||
"""
|
||||
function(n_clicks) {
|
||||
if (n_clicks == null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
""",
|
||||
Output("offcanvas", "is_open", allow_duplicate=True),
|
||||
Input({"type": "nav-link", "index": dash.dependencies.ALL}, "n_clicks"),
|
||||
prevent_initial_call="initial_duplicate",
|
||||
)
|
||||
|
||||
return app
|
54
src/ocab_modules/ocab_modules/standard/miniapp/main.py
Normal file
54
src/ocab_modules/ocab_modules/standard/miniapp/main.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogram import types
|
||||
from fastapi.middleware.wsgi import WSGIMiddleware
|
||||
|
||||
from ocab_core.modules_system.public_api import (
|
||||
Storage,
|
||||
get_module,
|
||||
set_chat_menu_button,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ocab_modules.standard.config import IConfig
|
||||
|
||||
config: "IConfig" = get_module("standard.config", "config")
|
||||
|
||||
|
||||
def get_link():
|
||||
pass
|
||||
|
||||
|
||||
def module_init():
|
||||
|
||||
config.register(
|
||||
"miniapp::prefix",
|
||||
"string",
|
||||
default_value="/webapp/",
|
||||
visible=False,
|
||||
)
|
||||
|
||||
config.register(
|
||||
"miniapp::public_url",
|
||||
"string",
|
||||
visible=False,
|
||||
)
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def register_page():
|
||||
pass
|
||||
|
||||
|
||||
async def module_late_init():
|
||||
from .lib import create_dash_app
|
||||
|
||||
dash_app = create_dash_app(requests_pathname_prefix=config.get("miniapp::prefix"))
|
||||
|
||||
Storage.set("webapp", WSGIMiddleware(dash_app.server))
|
||||
|
||||
web_app_info = types.WebAppInfo(url=config.get("miniapp::public_url"))
|
||||
menu_button = types.MenuButtonWebApp(text="Меню", web_app=web_app_info)
|
||||
|
||||
await set_chat_menu_button(menu_button)
|
36
src/ocab_modules/ocab_modules/standard/roles/README.md
Normal file
36
src/ocab_modules/ocab_modules/standard/roles/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Модуль Roles
|
||||
|
||||
Модуль `roles` управляет ролями пользователей.
|
||||
|
||||
## Роли
|
||||
|
||||
- `USER` - обычный пользователь.
|
||||
- `MODERATOR` - модератор.
|
||||
- `ADMIN` - администратор.
|
||||
- `BOT` - бот.
|
||||
|
||||
## Функциональность
|
||||
|
||||
- Проверка роли пользователя.
|
||||
- Получение имени роли по ID.
|
||||
- Получение ID роли по имени.
|
||||
|
||||
## Использование
|
||||
|
||||
1. Импортируйте класс `Roles`.
|
||||
2. Создайте экземпляр класса.
|
||||
3. Вызовите методы класса для проверки роли пользователя или получения имени роли.
|
||||
|
||||
## Пример
|
||||
|
||||
```python
|
||||
from ocab_core.modules_system.public_api import get_module
|
||||
|
||||
Roles = get_module("standard.roles", "Roles")
|
||||
|
||||
roles = Roles()
|
||||
|
||||
is_admin = await roles.check_admin_permission(user_id)
|
||||
|
||||
role_name = await roles.get_role_name(role_id)
|
||||
```
|
2
src/ocab_modules/ocab_modules/standard/roles/__init__.py
Normal file
2
src/ocab_modules/ocab_modules/standard/roles/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .main import module_init
|
||||
from .roles import Roles
|
@@ -6,7 +6,9 @@
|
||||
"version": "1.0.0",
|
||||
"privileged": true,
|
||||
"dependencies": {
|
||||
"standard.config": "^1.0.0",
|
||||
"standard.database": "^1.0.0"
|
||||
"required": {
|
||||
"standard.config": "^1.0.0",
|
||||
"standard.database": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
37
src/ocab_modules/ocab_modules/standard/roles/main.py
Normal file
37
src/ocab_modules/ocab_modules/standard/roles/main.py
Normal file
@@ -0,0 +1,37 @@
|
||||
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
|
||||
|
||||
|
||||
def module_init():
|
||||
config: "IConfig" = get_module("standard.config", "config")
|
||||
|
||||
config.register(
|
||||
"roles::admin",
|
||||
"number",
|
||||
default_value=2,
|
||||
visible=False,
|
||||
)
|
||||
config.register(
|
||||
"roles::moderator",
|
||||
"number",
|
||||
default_value=1,
|
||||
visible=False,
|
||||
)
|
||||
config.register(
|
||||
"roles::user",
|
||||
"number",
|
||||
default_value=0,
|
||||
visible=False,
|
||||
)
|
||||
config.register(
|
||||
"roles::bot",
|
||||
"number",
|
||||
default_value=3,
|
||||
visible=False,
|
||||
)
|
||||
|
||||
pass
|
@@ -1,9 +1,15 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ocab_core.modules_system.public_api import get_module
|
||||
|
||||
get_user_role = get_module("standard.database", "db_api.get_user_role")
|
||||
get_roles = get_module("standard.config", "get_roles")
|
||||
if TYPE_CHECKING:
|
||||
from ocab_modules.standard.config import IConfig
|
||||
from ocab_modules.standard.database.db_api import get_user_role as IGetUserRoleType
|
||||
|
||||
roles = get_roles()
|
||||
get_user_role: "IGetUserRoleType" = get_module(
|
||||
"standard.database", "db_api.get_user_role"
|
||||
)
|
||||
config: "IConfig" = get_module("standard.config", "config")
|
||||
|
||||
|
||||
class Roles:
|
||||
@@ -13,12 +19,16 @@ class Roles:
|
||||
bot = "BOT"
|
||||
|
||||
def __init__(self):
|
||||
self.user_role_id = roles[self.user]
|
||||
self.moderator_role_id = roles[self.moderator]
|
||||
self.admin_role_id = roles[self.admin]
|
||||
self.bot_role_id = roles[self.bot]
|
||||
pass
|
||||
|
||||
def update_roles(self):
|
||||
self.user_role_id = config.get("roles::user")
|
||||
self.moderator_role_id = config.get("roles::moderator")
|
||||
self.admin_role_id = config.get("roles::admin")
|
||||
self.bot_role_id = config.get("roles::bot")
|
||||
|
||||
async def check_admin_permission(self, user_id):
|
||||
self.update_roles()
|
||||
match get_user_role(user_id):
|
||||
case self.admin_role_id:
|
||||
return True
|
||||
@@ -26,6 +36,7 @@ class Roles:
|
||||
return False
|
||||
|
||||
async def check_moderator_permission(self, user_id):
|
||||
self.update_roles()
|
||||
match get_user_role(user_id):
|
||||
case self.moderator_role_id:
|
||||
return True
|
||||
@@ -33,6 +44,7 @@ class Roles:
|
||||
return False
|
||||
|
||||
async def get_role_name(self, role_id):
|
||||
self.update_roles()
|
||||
match role_id:
|
||||
case self.admin_role_id:
|
||||
return self.admin
|
||||
@@ -46,6 +58,7 @@ class Roles:
|
||||
raise ValueError(f"Нет роли с id={role_id}")
|
||||
|
||||
async def get_user_permission(self, user_id):
|
||||
self.update_roles()
|
||||
match get_user_role(user_id):
|
||||
case self.admin_role_id:
|
||||
return self.admin
|
1465
src/ocab_modules/poetry.lock
generated
Normal file
1465
src/ocab_modules/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
src/ocab_modules/poetry.toml
Normal file
2
src/ocab_modules/poetry.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[virtualenvs]
|
||||
in-project = true
|
25
src/ocab_modules/pyproject.toml
Normal file
25
src/ocab_modules/pyproject.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[tool.poetry]
|
||||
name = "ocab-modules"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Maxim Slipenko <maxim@slipenko.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "~3.12"
|
||||
ocab-core = { path = "../ocab_core", develop = true }
|
||||
|
||||
peewee = "^3.17.6"
|
||||
|
||||
pyyaml = "^6.0.1"
|
||||
|
||||
dash = "^2.17.1"
|
||||
dash-extensions = "^1.0.18"
|
||||
dash-bootstrap-components = "^1.6.0"
|
||||
|
||||
[tool.poetry-monorepo.deps]
|
||||
enabled = true
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"id": "standard.command_helper",
|
||||
"name": "Command helper",
|
||||
"description": "Модуль для отображения команд при вводе '/'",
|
||||
"author": "OCAB Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": false,
|
||||
"dependencies": {
|
||||
"standard.roles": "^1.0.0",
|
||||
"standard.database": "^1.0.0"
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
from .config import (
|
||||
get_approved_chat_id,
|
||||
get_default_chat_tag,
|
||||
get_roles,
|
||||
get_yandexgpt_in_words,
|
||||
get_yandexgpt_start_words,
|
||||
)
|
@@ -1,79 +0,0 @@
|
||||
# flake8: noqa
|
||||
|
||||
import yaml
|
||||
|
||||
from src.service import paths
|
||||
|
||||
|
||||
def get_config(is_test: bool = False) -> dict:
|
||||
if is_test:
|
||||
path = f"{paths.modules_standard}/config/tests"
|
||||
else:
|
||||
path = paths.core
|
||||
path = f"{path}/config.yaml"
|
||||
|
||||
with open(path, "r") as file:
|
||||
return yaml.full_load(file)
|
||||
|
||||
|
||||
config = get_config()
|
||||
|
||||
|
||||
def get_telegram_token() -> str:
|
||||
return config["TELEGRAM"]["TOKEN"]
|
||||
|
||||
|
||||
def get_telegram_check_bot() -> bool:
|
||||
return config["TELEGRAM"]["CHECK_BOT"]
|
||||
|
||||
|
||||
def get_approved_chat_id() -> list:
|
||||
# Возваращем сплитованный список id чатов в формате int
|
||||
return [
|
||||
int(chat_id) for chat_id in config["TELEGRAM"]["APPROVED_CHAT_ID"].split(" | ")
|
||||
]
|
||||
|
||||
|
||||
def get_roles():
|
||||
return config["ROLES"]
|
||||
|
||||
|
||||
def get_user_role_name(role_number) -> dict:
|
||||
# Возвращаем название роли пользвателя по номеру роли, если такой роли нет, возвращаем неизвестно
|
||||
return config["ROLES"].get(role_number, "Неизвестно")
|
||||
|
||||
|
||||
def get_default_chat_tag() -> str:
|
||||
return config["TELEGRAM"]["DEFAULT_CHAT_TAG"]
|
||||
|
||||
|
||||
def get_yandexgpt_token() -> str:
|
||||
return config["YANDEXGPT"]["TOKEN"]
|
||||
|
||||
|
||||
def get_yandexgpt_catalog_id() -> str:
|
||||
return config["YANDEXGPT"]["CATALOGID"]
|
||||
|
||||
|
||||
def get_yandexgpt_prompt() -> str:
|
||||
return config["YANDEXGPT"]["PROMPT"]
|
||||
|
||||
|
||||
def get_yandexgpt_start_words() -> list:
|
||||
return config["YANDEXGPT"]["STARTWORD"].split(" | ")
|
||||
|
||||
|
||||
def get_yandexgpt_in_words() -> list:
|
||||
return config["YANDEXGPT"]["INWORD"].split(" | ")
|
||||
|
||||
|
||||
def get_yandexgpt_token_for_request() -> int:
|
||||
return config["YANDEXGPT"]["TOKEN_FOR_REQUEST"]
|
||||
|
||||
|
||||
def get_yandexgpt_token_for_answer() -> int:
|
||||
return config["YANDEXGPT"]["TOKEN_FOR_ANSWER"]
|
||||
|
||||
|
||||
def get_access_rights() -> dict:
|
||||
return get_config()["ACCESS_RIGHTS"]
|
@@ -1,7 +0,0 @@
|
||||
TELEGRAM:
|
||||
TOKEN: xxxxxxxxxxxxxxxxxxxx
|
||||
ROLES:
|
||||
ADMIN: 0
|
||||
MODERATOR: 1
|
||||
USER: 2
|
||||
BOT: 3
|
@@ -1 +0,0 @@
|
||||
from .filters import ChatModerOrAdminFilter, ChatNotInApproveFilter
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user