0
0
mirror of https://gitflic.ru/project/maks1ms/ocab.git synced 2025-12-23 21:49:55 +03:00
This commit is contained in:
2024-07-20 12:01:00 +03:00
parent e8b5f79d99
commit d52864a231
26 changed files with 1241 additions and 94 deletions

View File

@@ -6,6 +6,8 @@
"version": "1.0.0",
"privileged": false,
"dependencies": {
"standard.filters": "^1.0.0"
"required": {
"standard.filters": "^1.0.0"
}
}
}

View File

@@ -1,12 +1,14 @@
{
"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"
"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"
}
}
}

View File

@@ -5,3 +5,4 @@ from .config import (
get_yandexgpt_in_words,
get_yandexgpt_start_words,
)
from .main import module_late_init

View File

@@ -1,22 +1,67 @@
# flake8: noqa
import yaml
from .config_manager import ConfigManager
from src.service import paths
config = ConfigManager(
config_path="/home/maxim/dev/alt-gnome-infrastructure/ocab/src/ocab_core/config.yaml"
)
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"
def register_settings(settings_manager: ConfigManager):
# TELEGRAM settings
settings_manager.register_setting(
["TELEGRAM", "TOKEN"], "", "string", is_private=True
)
settings_manager.register_setting(
["TELEGRAM", "APPROVED_CHAT_ID"],
"-123456789 | -012345678",
"string",
pretty_name="ID разрешенных чатов",
description='Чаты, в которых будет работать бот. "|" - разделитель',
)
settings_manager.register_setting(
["TELEGRAM", "ADMINCHATID"],
-12345678,
"number",
pretty_name="ID чата администраторов",
)
settings_manager.register_setting(
["TELEGRAM", "DEFAULT_CHAT_TAG"],
"@alt_gnome_chat",
"string",
pretty_name="Основной чат",
)
settings_manager.register_setting(["TELEGRAM", "CHECK_BOT"], True, "checkbox")
with open(path, "r") as file:
return yaml.full_load(file)
# 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")
config = get_config()
register_settings(config)
def get_telegram_token() -> str:
@@ -76,4 +121,4 @@ def get_yandexgpt_token_for_answer() -> int:
def get_access_rights() -> dict:
return get_config()["ACCESS_RIGHTS"]
return config["ACCESS_RIGHTS"]

View File

@@ -0,0 +1,259 @@
from typing import Any, Dict, List, Optional, Union
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 Input, Output, dcc, html
DASH_AVAILABLE = True
except ImportError:
DASH_AVAILABLE = False
class ConfigManager:
def __init__(self, config_path: str):
self._config_path = config_path
self._config = self.load_config()
self._registered_settings = dict()
self._registered_settings_meta = dict()
self._update_callbacks = []
self._update_required = False
@property
def config(self):
return self._config
@config.setter
def config(self, value):
self._config = value
def load_config(self) -> Dict[str, Any]:
with open(self._config_path, "r") as file:
return yaml.safe_load(file)
def save_config(self):
with open(self._config_path, "w") as file:
yaml.dump(self._config, file)
def register_setting(
self,
key: Union[str, List[str]],
default_value: Any,
setting_type: str,
is_private: bool = False,
pretty_name: str = None,
description: str = None,
options: Optional[List[str]] = None,
):
if isinstance(key, str):
key = [key]
current = self._registered_settings
for k in key[:-1]:
if k not in current:
current[k] = {}
current = current[k]
current[key[-1]] = self.get_nested_setting(self._config, key, default_value)
self.set_nested_setting(
self._registered_settings_meta,
key,
{
"type": setting_type,
"is_private": is_private,
"options": options,
"pretty_name": pretty_name,
"description": description,
},
)
def get_nested_setting(
self, config: dict, keys: List[str], default: Any = None
) -> Any:
current = config
for key in keys:
if key in current:
current = current[key]
else:
return default
return current
def set_nested_setting(self, config: dict, keys: List[str], value: Any):
current = config
for key in keys[:-1]:
if key not in current:
current[key] = {}
current = current[key]
current[keys[-1]] = value
def __getitem__(self, key):
if key in self._registered_settings:
return self._registered_settings[key]
raise KeyError(key)
def update_setting(self, key: Union[str, List[str]], value: Any):
if isinstance(key, str):
key = [key]
self.set_nested_setting(self._registered_settings, key, value)
self.set_nested_setting(self._config, key, value)
self.save_config()
def get_settings_layout(self):
from dash_extensions.enrich import DashBlueprint
bp = DashBlueprint()
def create_layout():
def create_nested_layout(settings: dict, key_list=None):
if key_list is None:
key_list = []
components = []
for key, value in settings.items():
current_key_list = key_list.copy()
current_key_list.append(key)
if isinstance(value, dict):
nested = create_nested_layout(value, current_key_list)
if len(nested) > 0:
components.append(
dbc.Card(
[
dbc.CardHeader(html.H3(key, className="mb-0")),
dbc.CardBody(nested),
],
className="mb-3",
)
)
else:
meta = self.get_nested_setting(
self._registered_settings_meta, current_key_list
)
if not meta.get("is_private"):
row = []
label_text = meta.get("pretty_name", key)
if meta.get("type") != "checkbox":
row.append(dbc.Label(label_text))
component_id = {
"type": "setting",
"key": "-".join(current_key_list),
}
if meta.get("type") == "string":
component = dbc.Input(
id=component_id, type="text", value=value
)
elif meta.get("type") == "number":
component = dbc.Input(
id=component_id, type="number", value=value
)
elif meta.get("type") == "checkbox":
component = dbc.Col(
[
dbc.Checkbox(
id=component_id,
value=value,
label=dbc.Label(
label_text,
style={"margin-right": "10px"},
check=True,
),
)
]
)
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
)
else:
continue
row.append(component)
if meta.get("description"):
row.append(dbc.FormText(meta.get("description")))
components.append(dbc.Row(row, className="mb-3"))
return components
settings_components = create_nested_layout(self._registered_settings)
layout = html.Div(
[
html.H1("Настройки"),
dbc.Form(settings_components),
dbc.Button(
"Сохранить",
id="save-settings",
color="primary",
className="mt-3",
n_clicks=0,
),
html.Div(id="settings-update-trigger", style={"display": "none"}),
html.Span(
id="save-confirmation", style={"verticalAlign": "middle"}
),
dcc.Store(id="settings-store"),
],
style={
"padding": "20px",
},
)
return layout
bp.layout = create_layout
self.setup_callbacks(bp)
return bp
def setup_callbacks(self, app):
# ws = WebSocket(app, url="/ws")
@app.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"),
running=[(Output("save-settings", "disabled"), True, False)],
)
def save_settings(n_clicks, values, ids):
if n_clicks > 0:
# updated_settings = {}
# print(ids)
# for value, id_dict in zip(values, ids):
# key = id_dict["key"]
# self.update_setting(key.split("-"), value)
# updated_settings[key] = value
return "Настройки сохранены!" # , json.dumps(updated_settings)
return "" # , None
# @app.callback(
# Output({"type": "setting", "key": ALL}, "value"),
# Input("settings-store", "data"),
# )
# def update_settings_from_store(data):
# if data:
# updated_settings = json.loads(data)
# print(
# [current_value for key, current_value in updated_settings.items()]
# )
# return [
# current_value for key, current_value in updated_settings.items()
# ]
# raise dash.exceptions.PreventUpdate()

View File

@@ -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"
}
}
}

View File

@@ -0,0 +1,35 @@
from ocab_core.modules_system.public_api import get_module, log
from .config import config
def register_settings_page():
try:
register_page = get_module("standard.miniapp", "register_page")
#
# def setup_callbacks_wrapper(config_manager):
# def setup(app):
# config_manager.setup_callbacks(app)
#
# return setup
#
# register_page(
# name="Настройки",
# path="/settings",
# layout=config.get_settings_layout(),
# setup_callbacks=setup_callbacks_wrapper(config),
# )
register_page(
name="Настройки", path="/settings", blueprint=config.get_settings_layout()
)
pass
except Exception as e:
log(str(e))
pass
def module_late_init():
register_settings_page()
pass

View File

@@ -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"
}
}
}

View File

@@ -1,11 +1,13 @@
{
"id": "standard.fsm_database_storage",
"name": "FSM Database Storage",
"description": "Очень полезный модуль",
"author": "OCAB Team",
"version": "1.0.0",
"privileged": false,
"dependencies": {
"standard.database": "^1.0.0"
"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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -1 +1,2 @@
from .main import module_init
from .lib import register_page
from .main import module_init, module_late_init

View File

@@ -5,5 +5,17 @@
"author": "OCAB Team",
"version": "1.0.0",
"privileged": false,
"dependencies": {}
"dependencies": {
"required": {
"standard.config": {
"version": "^1.0.0",
"uses": []
}
}
},
"pythonDependencies": {
"required": {
"flet": "^0.23.2"
}
}
}

View File

@@ -0,0 +1,172 @@
from collections import OrderedDict
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
pages = OrderedDict()
def register_page(name, path, blueprint):
pages[path] = {
"name": name,
"blueprint": blueprint,
}
def register_home_page():
page = DashBlueprint()
page.layout = html.Div([html.H1("Главная")])
register_page("Главная", path="/", blueprint=page)
register_home_page()
def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash:
server = Flask(__name__)
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"
], # Add Telegram Mini Apps script to <head>
server=server,
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():
# dash.register_page(page["name"], path=path, layout=page["layout"])
page["blueprint"].register(app, path, prefix="a")
# Create sidebar
sidebar = dbc.Offcanvas(
id="offcanvas",
title="Меню",
is_open=False,
children=[
dbc.Nav(
[
dbc.NavLink(
page["name"],
href=f"{requests_pathname_prefix}{path.lstrip('/')}",
id={"type": "nav-link", "index": path},
)
for path, page in pages.items()
],
vertical=True,
pills=True,
),
],
)
# 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,
)
# Define app layout
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,
setup_page_components(),
]
)
# 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"),
)
# Открытие на кнопку меню
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

View File

@@ -1,2 +1,31 @@
from aiogram import types
from fastapi.middleware.wsgi import WSGIMiddleware
from ocab_core.modules_system.public_api import Storage, set_chat_menu_button
def get_link():
pass
def module_init():
pass
def register_page():
pass
async def module_late_init():
from .lib import create_dash_app
dash_app = create_dash_app(requests_pathname_prefix="/webapp/")
Storage.set("webapp", WSGIMiddleware(dash_app.server))
web_app_info = types.WebAppInfo(
url="https://mackerel-pumped-foal.ngrok-free.app/webapp"
)
menu_button = types.MenuButtonWebApp(text="Меню", web_app=web_app_info)
await set_chat_menu_button(menu_button)

View File

@@ -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"
}
}
}