diff --git a/src/ocab_core/main.py b/src/ocab_core/main.py index c42185b..825b034 100644 --- a/src/ocab_core/main.py +++ b/src/ocab_core/main.py @@ -10,11 +10,9 @@ from ocab_core.lib import get_module_directory, register_bot_webhook from ocab_core.logger import CustomLogger, log, setup_logger from ocab_core.modules_system import ModulesManager from ocab_core.modules_system.loaders import FSLoader, UnsafeFSLoader +from ocab_core.modules_system.public_api import get_module from ocab_core.singleton import Singleton -# TODO: заменить на get_module("standard.config") -from ocab_modules.standard.config.config import get_telegram_token - ocab_modules_path = get_module_directory("ocab_modules") @@ -28,15 +26,15 @@ def ocab_modules_loader(namespace: str, module_name: str, safe=True): bot_modules = [ ocab_modules_loader("standard", "config", safe=False), ocab_modules_loader("standard", "database", safe=False), - # ocab_modules_loader("standard", "fsm_database_storage", safe=False), + ocab_modules_loader("standard", "fsm_database_storage", safe=False), ocab_modules_loader("standard", "roles", safe=False), - ocab_modules_loader("external", "yandexgpt", safe=False), + ocab_modules_loader("external", "yandexgpt", safe=True), # ocab_modules_loader("standard", "command_helper"), - # ocab_modules_loader("standard", "info"), - # ocab_modules_loader("standard", "filters"), - # ocab_modules_loader("external", "create_report_apps"), - # ocab_modules_loader("standard", "admin"), + ocab_modules_loader("standard", "info"), + ocab_modules_loader("standard", "filters"), + ocab_modules_loader("external", "create_report_apps"), + ocab_modules_loader("standard", "admin"), ocab_modules_loader("standard", "message_processing"), ocab_modules_loader("standard", "miniapp", safe=False), ] @@ -69,7 +67,6 @@ async def init_app(): singleton = Singleton() try: - singleton.bot = Bot(token=get_telegram_token()) singleton.modules_manager = ModulesManager() for module_loader in bot_modules: @@ -77,6 +74,9 @@ async def init_app(): log(f"Loading {info.name} ({info.id}) module") await singleton.modules_manager.load(module_loader) + get_telegram_token = get_module("standard.config", ["get_telegram_token"]) + + singleton.bot = Bot(token=get_telegram_token()) singleton.dp = Dispatcher(storage=singleton.storage["_fsm_storage"]) singleton.dp.include_routers(*singleton.storage["_routers"]) diff --git a/src/ocab_core/modules_system/modules_manager.py b/src/ocab_core/modules_system/modules_manager.py index 81c1d0f..390e826 100644 --- a/src/ocab_core/modules_system/modules_manager.py +++ b/src/ocab_core/modules_system/modules_manager.py @@ -1,3 +1,4 @@ +import importlib import inspect import pkg_resources @@ -22,6 +23,9 @@ def is_version_compatible(version, requirement): return [req] for r in parse_requirement(requirement): + if r == "*": + continue + if not semver.Version.parse(version).match(r): return False @@ -32,13 +36,18 @@ def check_python_dependencies(info: ModuleInfo): if info.pythonDependencies and info.pythonDependencies.required: for dependency, req in info.pythonDependencies.required.items(): try: - installed_version = pkg_resources.get_distribution(dependency).version - except pkg_resources.DistributionNotFound: + importlib.import_module(dependency) + except ImportError: raise Exception( - f"Module {info.id} requires {dependency}," + 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): diff --git a/src/ocab_modules/external/yandexgpt/__init__.py b/src/ocab_modules/external/yandexgpt/__init__.py index cb94c2b..7aecf7f 100644 --- a/src/ocab_modules/external/yandexgpt/__init__.py +++ b/src/ocab_modules/external/yandexgpt/__init__.py @@ -1 +1,2 @@ from .handlers import answer_to_message +from .main import module_init diff --git a/src/ocab_modules/external/yandexgpt/handlers.py b/src/ocab_modules/external/yandexgpt/handlers.py index 3505630..86525b5 100644 --- a/src/ocab_modules/external/yandexgpt/handlers.py +++ b/src/ocab_modules/external/yandexgpt/handlers.py @@ -2,13 +2,16 @@ 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 + +(get_yandexgpt_catalog_id, get_yandexgpt_token, get_yandexgpt_prompt) = get_module( + "standard.config", + ["get_yandexgpt_catalog_id", "get_yandexgpt_token", "get_yandexgpt_prompt"], ) -from ocab_modules.standard.database.db_api import add_message + +add_message = get_module("standard.database", "db_api.add_message") async def answer_to_message(message: Message, bot: Bot): diff --git a/src/ocab_modules/external/yandexgpt/info.json b/src/ocab_modules/external/yandexgpt/info.json index 6deed05..3cdf490 100644 --- a/src/ocab_modules/external/yandexgpt/info.json +++ b/src/ocab_modules/external/yandexgpt/info.json @@ -4,6 +4,18 @@ "description": "Модуль для работы с Yandex GPT", "author": "OCAB Team", "version": "1.0.0", - "privileged": true, - "dependencies": {} + "privileged": false, + "dependencies": { + "required": { + "standard.config": "^1.0.0", + "standard.database": "^1.0.0" + } + }, + "pythonDependencies": { + "required": { + "aiohttp": "*", + "requests": "*", + "json": "*" + } + } } diff --git a/src/ocab_modules/external/yandexgpt/main.py b/src/ocab_modules/external/yandexgpt/main.py new file mode 100644 index 0000000..be002cd --- /dev/null +++ b/src/ocab_modules/external/yandexgpt/main.py @@ -0,0 +1,15 @@ +from ocab_core.modules_system.public_api import get_module + +config = get_module("standard.config", "config") + + +def module_init(): + config.register_setting(["YANDEXGPT", "TOKEN"], "", "string", is_private=True) + config.register_setting(["YANDEXGPT", "TOKEN_FOR_REQUEST"], 8000, "number") + config.register_setting(["YANDEXGPT", "TOKEN_FOR_ANSWER"], 2000, "number") + config.register_setting(["YANDEXGPT", "CATALOGID"], "", "string", is_private=True) + config.register_setting(["YANDEXGPT", "PROMPT"], "Ты чат-бот ...", "string") + config.register_setting( + ["YANDEXGPT", "STARTWORD"], "Бот| Бот, | бот | бот,", "string" + ) + config.register_setting(["YANDEXGPT", "INWORD"], "помогите | не работает", "string") diff --git a/src/ocab_modules/external/yandexgpt/routers.py b/src/ocab_modules/external/yandexgpt/routers.py index 66c4230..9f61d5f 100644 --- a/src/ocab_modules/external/yandexgpt/routers.py +++ b/src/ocab_modules/external/yandexgpt/routers.py @@ -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 diff --git a/src/ocab_modules/external/yandexgpt/yandexgpt.py b/src/ocab_modules/external/yandexgpt/yandexgpt.py index b409d5b..ae3be54 100644 --- a/src/ocab_modules/external/yandexgpt/yandexgpt.py +++ b/src/ocab_modules/external/yandexgpt/yandexgpt.py @@ -1,14 +1,25 @@ # flake8: noqa -import asyncio import json 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 * +db_api = get_module("standard.database", "db_api") + +( + get_yandexgpt_token_for_answer, + get_yandexgpt_prompt, + get_yandexgpt_token_for_request, +) = get_module( + "standard.config", + [ + "get_yandexgpt_token_for_answer", + "get_yandexgpt_prompt", + "get_yandexgpt_token_for_request", + ], +) class YandexGPT: @@ -108,8 +119,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 = { diff --git a/src/ocab_modules/standard/config/__init__.py b/src/ocab_modules/standard/config/__init__.py index da07248..9dc3796 100644 --- a/src/ocab_modules/standard/config/__init__.py +++ b/src/ocab_modules/standard/config/__init__.py @@ -1,8 +1,13 @@ from .config import ( + config, get_approved_chat_id, get_default_chat_tag, get_roles, + get_telegram_token, get_yandexgpt_in_words, + get_yandexgpt_prompt, get_yandexgpt_start_words, + get_yandexgpt_token_for_answer, + get_yandexgpt_token_for_request, ) from .main import module_late_init diff --git a/src/ocab_modules/standard/config/config.py b/src/ocab_modules/standard/config/config.py index 2caa089..e7453aa 100644 --- a/src/ocab_modules/standard/config/config.py +++ b/src/ocab_modules/standard/config/config.py @@ -33,33 +33,6 @@ def register_settings(settings_manager: ConfigManager): ) settings_manager.register_setting(["TELEGRAM", "CHECK_BOT"], True, "checkbox") - # YANDEXGPT settings - settings_manager.register_setting( - ["YANDEXGPT", "TOKEN"], "", "string", is_private=True - ) - settings_manager.register_setting( - ["YANDEXGPT", "TOKEN_FOR_REQUEST"], 8000, "number" - ) - settings_manager.register_setting(["YANDEXGPT", "TOKEN_FOR_ANSWER"], 2000, "number") - settings_manager.register_setting( - ["YANDEXGPT", "CATALOGID"], "", "string", is_private=True - ) - settings_manager.register_setting( - ["YANDEXGPT", "PROMPT"], "Ты чат-бот ...", "string" - ) - settings_manager.register_setting( - ["YANDEXGPT", "STARTWORD"], "Бот| Бот, | бот | бот,", "string" - ) - settings_manager.register_setting( - ["YANDEXGPT", "INWORD"], "помогите | не работает", "string" - ) - - # ROLES settings - settings_manager.register_setting(["ROLES", "ADMIN"], 2, "number") - settings_manager.register_setting(["ROLES", "MODERATOR"], 1, "number") - settings_manager.register_setting(["ROLES", "USER"], 0, "number") - settings_manager.register_setting(["ROLES", "BOT"], 3, "number") - register_settings(config) diff --git a/src/ocab_modules/standard/config/config_manager.py b/src/ocab_modules/standard/config/config_manager.py index 4332701..7a8aecc 100644 --- a/src/ocab_modules/standard/config/config_manager.py +++ b/src/ocab_modules/standard/config/config_manager.py @@ -1,10 +1,8 @@ -import time from typing import Any, Dict, List, Optional, Union +import flask import yaml -# from ocab_core.modules_system.public_api import get_module, log - try: import dash_bootstrap_components as dbc from dash_extensions.enrich import ALL, Input, Output, State, dcc, html @@ -37,7 +35,7 @@ class ConfigManager: def save_config(self): with open(self._config_path, "w") as file: - yaml.dump(self._config, file) + yaml.dump(self._config, file, allow_unicode=True) def register_setting( self, @@ -244,7 +242,8 @@ class ConfigManager: # ] ) def save_settings(n_clicks, values, keys): - time.sleep(3) + print(flask.g.user) + if n_clicks > 0: updated_settings = {} diff --git a/src/ocab_modules/standard/miniapp/dash_telegram_auth.py b/src/ocab_modules/standard/miniapp/dash_telegram_auth.py new file mode 100644 index 0000000..e181898 --- /dev/null +++ b/src/ocab_modules/standard/miniapp/dash_telegram_auth.py @@ -0,0 +1,60 @@ +import hashlib +import hmac +import time +from urllib.parse import parse_qsl + +import flask +from aiogram.utils.web_app import safe_parse_webapp_init_data +from dash import Dash +from dash_extensions.enrich import Input +from flask import request + + +def get_auth_server(bot_token: str): + server = flask.Flask(__name__) + + def validate_init_data(init_data): + try: + init_data = dict(parse_qsl(init_data)) + received_hash = init_data.pop("hash") + data_check_string = "\n".join( + f"{k}={v}" for k, v in sorted(init_data.items()) + ) + secret_key = hmac.new( + b"WebAppData", bot_token.encode(), hashlib.sha256 + ).digest() + calculated_hash = hmac.new( + secret_key, data_check_string.encode(), hashlib.sha256 + ).hexdigest() + + auth_date = int(init_data.get("auth_date", 0)) + if (time.time() - auth_date) > 86400: + return False, "Init data is outdated" + + return calculated_hash == received_hash, init_data + except Exception as e: + return False, str(e) + + @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_clientcallback(app: Dash): + app.clientside_callback( + """ + function(n_inervals) { + const tg = window.Telegram.WebApp; + document.cookie = `tg_init_data=${JSON.stringify(tg.initData)}; path=/`; + } + """, + Input("init-telegram-interval", "n_intervals"), + ) diff --git a/src/ocab_modules/standard/miniapp/lib.py b/src/ocab_modules/standard/miniapp/lib.py index e72f9ba..79ea09d 100644 --- a/src/ocab_modules/standard/miniapp/lib.py +++ b/src/ocab_modules/standard/miniapp/lib.py @@ -4,7 +4,11 @@ import dash import dash_bootstrap_components as dbc from dash_extensions.enrich import DashBlueprint, DashProxy, Input, Output, dcc, html from dash_extensions.pages import setup_page_components -from flask import Flask + +# TODO: заменить на get_module("standard.config") +from ocab_modules.standard.config.config import get_telegram_token + +from .dash_telegram_auth import get_auth_server, setup_auth_clientcallback pages = OrderedDict() @@ -27,7 +31,8 @@ register_home_page() def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash: - server = Flask(__name__) + server = get_auth_server(get_telegram_token()) + app = DashProxy( pages_folder="", use_pages=True, @@ -46,10 +51,10 @@ def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash: ], ) - app.enable_dev_tools( - dev_tools_ui=True, - dev_tools_serve_dev_bundles=True, - ) + # app.enable_dev_tools( + # dev_tools_ui=True, + # dev_tools_serve_dev_bundles=True, + # ) # Register pages for path, page in pages.items(): @@ -98,15 +103,12 @@ def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash: app.layout = html.Div( [ dcc.Location(id="url", refresh=False), - # dcc.Store(id="user-data", storage_type="session"), dcc.Interval( id="init-telegram-interval", interval=100, n_intervals=0, max_intervals=1, ), - # WebSocket(url="/ws"), - html.Div(id="telegram-login-info"), navbar, sidebar, dash.page_container, @@ -114,29 +116,7 @@ def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash: ] ) - # Clientside callback to initialize Telegram Mini Apps and get user data - app.clientside_callback( - """ - function(n_intervals) { - return new Promise((resolve, reject) => { - resolve("test"); - if (window.Telegram && window.Telegram.WebApp) { - const webapp = window.Telegram.WebApp; - webapp.ready(); - if (webapp.initDataUnsafe && webapp.initDataUnsafe.user) { - resolve(webapp.initDataUnsafe.user); - } else { - reject("User not authorized"); - } - } else { - reject("Telegram Mini Apps not available"); - } - }); - } - """, - Output("user-data", "data"), - Input("init-telegram-interval", "n_intervals"), - ) + setup_auth_clientcallback(app) # Открытие на кнопку меню app.clientside_callback( @@ -155,7 +135,7 @@ def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash: Input("open-offcanvas", "n_clicks"), ) - # # Закрываем offcanvas при клике на ссылку в меню + # Закрываем offcanvas при клике на ссылку в меню app.clientside_callback( """ function(n_clicks) { diff --git a/src/ocab_modules/standard/roles/__init__.py b/src/ocab_modules/standard/roles/__init__.py index 5cd56b0..714df8c 100644 --- a/src/ocab_modules/standard/roles/__init__.py +++ b/src/ocab_modules/standard/roles/__init__.py @@ -1 +1,2 @@ +from .main import module_init from .roles import Roles diff --git a/src/ocab_modules/standard/roles/main.py b/src/ocab_modules/standard/roles/main.py new file mode 100644 index 0000000..08ec767 --- /dev/null +++ b/src/ocab_modules/standard/roles/main.py @@ -0,0 +1,12 @@ +from ocab_core.modules_system.public_api import get_module + + +def module_init(): + config = get_module("standard.config", "config") + + config.register_setting(["ROLES", "ADMIN"], 2, "number", is_private=True) + config.register_setting(["ROLES", "MODERATOR"], 1, "number", is_private=True) + config.register_setting(["ROLES", "USER"], 0, "number", is_private=True) + config.register_setting(["ROLES", "BOT"], 3, "number", is_private=True) + + pass diff --git a/src/ocab_modules/standard/roles/roles.py b/src/ocab_modules/standard/roles/roles.py index 7c67614..3ae5947 100644 --- a/src/ocab_modules/standard/roles/roles.py +++ b/src/ocab_modules/standard/roles/roles.py @@ -3,8 +3,6 @@ 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") -roles = get_roles() - class Roles: user = "USER" @@ -13,12 +11,17 @@ class Roles: bot = "BOT" def __init__(self): + pass + + def update_roles(self): + roles = get_roles() 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] 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 +29,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 +37,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 +51,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