0
0
mirror of https://gitflic.ru/project/maks1ms/ocab.git synced 2024-12-24 16:44:44 +03:00

Compare commits

...

2 Commits

Author SHA1 Message Date
2becd33774 wip 2024-08-01 11:16:08 +03:00
9fa23f18b9 обновил документацию 2024-07-31 20:01:32 +03:00
10 changed files with 352 additions and 152 deletions

View File

@ -1,60 +1,44 @@
# OpenChatAiBot V2 # OCAB - Open Chat Ai Bot
## Что такое OCAB? ## Что такое OCAB?
OCAB - это бот для Telegram, который призван помочь во взаимодействии с чатом. OCAB - это платформа для разработки модульных Telegram-ботов, которая призвана упростить взаимодействие с чатами.
Бот поддерживает интеграцию модулей для расширения функционала. OCAB предоставляет возможность расширять функциональность бота с помощью интеграции различных модулей.
Фактически бот является платформой для запуска созданных для него модулей. Код платформы и набор стандартных модулей находятся в этом монорепозитории.
Модули могут взаимодействовать друг с другом или быть полностью независимыми.
## Что такое модуль? ## Структура монорепозитория
Модуль - это директория, которая содержит в себе код модуля и его конфигурацию. Монорепозиторий OCAB включает в себя:
* **Ядро OCAB (src/ocab_core):** Содержит основные компоненты платформы, такие как система управления модулями,
логирование и утилиты.
* **Модули OCAB (src/ocab_modules):** Содержит стандартные и дополнительные модули, которые расширяют
функциональность ботов OCAB.
* **Пример бота (src/gnomik):** Пример реализации бота на платформе OCAB.
## Модули
Модули OCAB - это независимые компоненты, которые добавляют функциональность к боту.
### Структура модуля ### Структура модуля
*Будет дополнено после закрытия [issue #17](https://gitflic.ru/project/armatik/ocab/issue/17).* Структура модуля представлена [здесь](docs/MODULES-SPEC.md).
## Стандартные модули ### Стандартные модули
В стандартный состав бота входят следующие модули: Стандартные модули предоставляют базовые функции для работы бота:
* [admin](src/ocab_modules/ocab_modules/standard/admin/README.md) - модуль для модерирования чата.
* [roles](src/ocab_modules/ocab_modules/standard/roles/README.md) - модуль ролей пользователей.
* [config](src/ocab_modules/ocab_modules/standard/config/README.md) - модуль управления конфигурацией бота.
* [database](src/ocab_modules/ocab_modules/standard/database/README.md) - модуль для работы с базой данных.
* [fsm_database_storage](src/ocab_modules/ocab_modules/standard/fsm_database_storage/README.md) - модуль для хранения состояний FSM в базе данных.
* [filters](src/ocab_modules/ocab_modules/standard/filters/README.md) - модуль, предоставляющий фильтры для aiogram.
* [message_processing](src/ocab_modules/ocab_modules/standard/message_processing/README.md) - модуль обработки входящих сообщений.
* [miniapp](src/ocab_modules/ocab_modules/standard/miniapp/README.md) - модуль для реализации веб-интерфейса бота.
* [command_helper](src/ocab_modules/ocab_modules/standard/command_helper/README.md) - модуль для упрощения регистрации команд бота.
* [info](src/ocab_modules/ocab_modules/standard/info/README.md) - модуль предоставления информации о пользователях и чатах.
* `admin` - модуль для модерирования чата. Позволяет удалять сообщения, банить пользователей и т.д. ### Дополнительные официальные модули
* `reputation` - модуль репутации пользователей. Позволяет оценивать ответы пользователей и накапливать репутацию.
* `welcome` - модуль приветствия новых пользователей. Позволяет приветствовать новых пользователей в чате, а также
проверять пользователя капчей для предотвращения спама.
* `roles` - модуль ролей. Позволяет назначать пользователям роли и ограничивать доступ к командам бота по ролям.
Является важной частью системы прав доступа и модуля `admin`.
## Дополнительные официальные модули Дополнительные официальные модули разработаны командой OCAB и предоставляют расширенные возможности для бота:
* [yandexgpt](src/ocab_modules/ocab_modules/external/yandexgpt/README.md) - модуль для интеграции с нейросетью YandexGPT.
* `yandexgpt` - модуль для генерации ответов на основе нейросети GPT-3.5. Позволяет боту отвечать на сообщения * [create_report_apps](src/ocab_modules/ocab_modules/external/create_report_apps/README.md) - модуль для создания отчетов об ошибках.
пользователей, используя нейросеть. Ключевой особенностью является построение линии контекста для нейросети,
которая позволяет боту отвечать на вопросы, используя контекст предыдущих сообщений. Для этого используется
модуль база данных хранящий историю сообщений.
<!--
* `bugzilla` - модуль для интеграции с BugZilla. Позволяет получать уведомления о новых багах в BugZilla, отслеживать их
статус, формировать стандартизированные сообщения для корректного описания багов. В будущем планируется интеграция с
API BugZilla для возможности создания багов из чата.
* `alt_packages` - модуль для интеграции с AltLinux Packages. Позволяет получать уведомления о новых пакетах в репозитории
AltLinux, поиска пакетов по названию, получения истории изменений, команды для установки пакета из репозитория и
прочей информации о пакете.
* `notes` - модуль заметок. Позволяет сохранять заметки для пользователей и чатов. Заметки являются ссылками на
сообщения в чате.
-->
Список модулей будет пополняться. Идеи для модулей можно оставлять в [issues](https://gitflic.ru/project/armatik/ocab/issue/create).
## Установка бота
### Docker
### Вручную
## Технологический стек
* Python 3.11.6 или выше - основной язык программирования.
* SQLite 3 - база данных для хранения информации о чате и пользователях.
* [Poetry](https://gitflic.ru/project/armatik/ocab/blob?file=how-to%20install%20deps.md&branch=OCAB-V2) - менеджер зависимостей.
* aiogram 3 - библиотека для работы с Telegram API.
* peewee - ORM для работы с базой данных.

39
docs/DEV.md Normal file
View File

@ -0,0 +1,39 @@
## Настройка рабочего окружения
Данная инструкция поможет вам настроить рабочее окружение для разработки OCAB.
### Предварительные требования
* **Python 3.12:** OCAB требует Python 3.12.
* **VSCode:** Рекомендуется использовать VSCode для разработки.
* **Git:** У вас должен быть установлен Git для клонирования репозитория.
### Шаги
1. **Клонируйте репозиторий:**
```bash
git clone https://gitflic.ru/project/armatik/ocab.git
```
2. **Откройте проект в VSCode:**
* Откройте папку `ocab` в VSCode.
* VSCode автоматически предложит открыть проект как workspace, используя файл `ocab.code-workspace`.
Нажмите "Открыть Workspace", чтобы принять предложение.
3. **Настройте Poetry:**
* Установите Poetry, следуя инструкциям на официальном сайте: [https://python-poetry.org/docs/](https://python-poetry.org/docs/).
* **Для каждого пакета:**
* Перейдите в папку пакета (например, `src/ocab_core`).
* Выполните команду `poetry install`, чтобы установить зависимости пакета.
* Poetry создаст виртуальное окружение внутри папки пакета (`.venv`).
4. **Активируйте виртуальное окружение:**
* Выполните команду `poetry shell` в папке пакета, чтобы активировать виртуальное окружение.
Теперь ваше рабочее окружение настроено, и вы можете начать.
### Дополнительная информация
* Каждый пакет в монорепозитории имеет свой собственный файл `pyproject.toml`, где указаны его зависимости.
* Poetry автоматически управляет виртуальными окружениями для каждого пакета.
* Вы можете использовать команду `poetry add <package_name>` для добавления новых зависимостей.

View File

@ -1,47 +0,0 @@
## Poetry
### Установка с официального сайта
```shell
curl -sSL https://install.python-poetry.org | python3 -
```
### Установка с PyPi
```shell
python3 -m pip install poetry
```
Доп информация:https://www.8host.com/blog/ustanovka-menedzhera-zavisimostej-poetry/
## Зависимости
### Добавление зависимости
```shell
poetry add NAME
```
`NAME` - название зависимости.
### Установка зависимостей
```shell
poetry install
```
### Обновление зависимостей
```shell
poetry update
```
## Виртуальное окружение
### Создание/активация
```shell
poetry shell
```
### Настройка
Хранить окружение внутри проекта
```shell
poetry config virtualenvs.in-project true
```

View File

@ -22,11 +22,29 @@ TODO: описать функционал
## Запуск ## Запуск
Запуск бота осуществляется с помощью команды: ### Docker
```bash 1. Соберите Docker-образ:
python -m gnomik ```bash
``` docker build -t gnomik .
```
2. Запустите контейнер:
```bash
docker run -p 9000:9000 -v ./config.yaml:/app/config.yaml -v ./database:/app/database gnomik
```
Замените `./config.yaml` и `./database` на пути к вашим локальным файлам конфигурации и паки для базы данных.
### Вручную
1. Активируйте виртуальное окружение Gnomика:
```bash
poetry shell
```
2. Запустите бота:
```bash
python -m gnomik
```
## Конфигурация ## Конфигурация

View File

@ -0,0 +1,18 @@
core:
mode: WEBHOOK
token: xxx
webhook:
public_url: xxx
filters:
approved_chat_id: -4128011756 | -4128011756
default_chat_tag: '@alt_gnome_chat'
miniapp:
public_url: xxx
yandexgpt:
catalogid: xxx
inword: помогите | не работает
prompt: Ты чат-бот ...
startword: Бот| Бот, | бот | бот,
token: xxx
token_for_answer: 2000
token_for_request: 8000

View File

@ -78,7 +78,7 @@ class OCAB:
singleton = Singleton() singleton = Singleton()
app = FastAPI() app = FastAPI()
config = get_module("standard.config", "config") config = get_module("standard.config", "config")
app.mount("/webapp", singleton.storage["webapp"]) app.mount(config.get("miniapp::prefix"), singleton.storage["webapp"])
await register_bot_webhook(app, singleton.bot, singleton.dp) await register_bot_webhook(app, singleton.bot, singleton.dp)
await singleton.bot.set_webhook(config.get("core::webhook::public_url")) await singleton.bot.set_webhook(config.get("core::webhook::public_url"))
hyperConfig = HyperConfig() hyperConfig = HyperConfig()

View File

@ -15,6 +15,7 @@ def register_settings_page():
path="/settings", path="/settings",
blueprint=get_miniapp_blueprint(config, prefix), blueprint=get_miniapp_blueprint(config, prefix),
prefix=prefix, prefix=prefix,
role="ADMIN",
) )
pass pass

View File

@ -1,13 +1,22 @@
import asyncio
from typing import TYPE_CHECKING
from .config_manager import ConfigManager from .config_manager import ConfigManager
try: try:
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
import flask
from dash_extensions.enrich import ALL, Input, Output, State, dcc, html from dash_extensions.enrich import ALL, Input, Output, State, dcc, html
DASH_AVAILABLE = True DASH_AVAILABLE = True
except ImportError: except ImportError:
DASH_AVAILABLE = False 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): def create_control(key: str, config: ConfigManager):
value = config.get(key) value = config.get(key)
@ -126,6 +135,10 @@ def create_settings_components(tree, level=0):
def get_miniapp_blueprint(config: ConfigManager, prefix: str): def get_miniapp_blueprint(config: ConfigManager, prefix: str):
Roles: "type[IRoles]" = get_module("standard.roles", "Roles")
roles = Roles()
import datetime import datetime
from dash_extensions.enrich import DashBlueprint from dash_extensions.enrich import DashBlueprint
@ -171,6 +184,28 @@ def get_miniapp_blueprint(config: ConfigManager, prefix: str):
) )
def save_settings(n_clicks, values, keys): def save_settings(n_clicks, values, keys):
if n_clicks > 0: 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: добавить валидацию значений # TODO: добавить валидацию значений
updated_settings = {} updated_settings = {}
@ -178,12 +213,18 @@ def get_miniapp_blueprint(config: ConfigManager, prefix: str):
key: str = id_dict["key"] key: str = id_dict["key"]
if prefix: if prefix:
key = key.removeprefix(f"{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 updated_settings[key] = value
config.mass_set(updated_settings) config.mass_set(updated_settings)
config.save() config.save()
# locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
now = datetime.datetime.now() now = datetime.datetime.now()
date_str = now.strftime("%H:%M:%S") date_str = now.strftime("%H:%M:%S")

View File

@ -1,13 +1,104 @@
import flask import flask
from aiogram.utils.web_app import safe_parse_webapp_init_data from aiogram.utils.web_app import safe_parse_webapp_init_data
from dash import Dash from dash import Dash
from dash_extensions.enrich import Input from dash_extensions.enrich import Input, Output
from flask import request 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): def get_auth_server(bot_token: str):
server = flask.Flask(__name__) 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 @server.before_request
def add_auth_data(): def add_auth_data():
init_data = request.cookies.get("tg_init_data") init_data = request.cookies.get("tg_init_data")
@ -21,13 +112,35 @@ def get_auth_server(bot_token: str):
return server return server
def setup_auth_clientcallback(app: Dash): def setup_auth_clientcallbacks(app: Dash):
app.clientside_callback( app.clientside_callback(
""" """
function(n_inervals) { function(n_intervals) {
const tg = window.Telegram.WebApp; if (window.webAppData) {
document.cookie = `tg_init_data=${JSON.stringify(tg.initData)}; path=/`; 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;
} }
""", """,
Input("init-telegram-interval", "n_intervals"), 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"),
) )

View File

@ -1,26 +1,30 @@
import asyncio
from collections import OrderedDict from collections import OrderedDict
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import dash import dash
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
import flask
from dash_extensions.enrich import DashBlueprint, DashProxy, Input, Output, dcc, html from dash_extensions.enrich import DashBlueprint, DashProxy, Input, Output, dcc, html
from dash_extensions.pages import setup_page_components from dash_extensions.pages import setup_page_components
from ocab_core.modules_system.public_api import get_module from ocab_core.modules_system.public_api import get_module, log
from .dash_telegram_auth import get_auth_server, setup_auth_clientcallback from .dash_telegram_auth import get_auth_server, setup_auth_clientcallbacks
if TYPE_CHECKING: if TYPE_CHECKING:
from ocab_modules.standard.config import IConfig from ocab_modules.standard.config import IConfig
from ocab_modules.standard.roles import Roles as IRoles
pages = OrderedDict() pages = OrderedDict()
def register_page(name, path, blueprint, prefix=""): def register_page(name, path, blueprint, prefix="", role="USER"):
pages[path] = { pages[path] = {
"name": name, "name": name,
"blueprint": blueprint, "blueprint": blueprint,
"prefix": prefix, "prefix": prefix,
"role": role,
} }
@ -33,9 +37,14 @@ def register_home_page():
register_home_page() register_home_page()
config: "IConfig" = get_module("standard.config", "config") 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: 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")) server = get_auth_server(config.get("core::token"))
app = DashProxy( app = DashProxy(
@ -47,10 +56,13 @@ def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash:
dbc.icons.BOOTSTRAP, dbc.icons.BOOTSTRAP,
], ],
external_scripts=[ external_scripts=[
#
"https://telegram.org/js/telegram-web-app.js" "https://telegram.org/js/telegram-web-app.js"
], # Add Telegram Mini Apps script to <head> ],
server=server, server=server,
requests_pathname_prefix=requests_pathname_prefix, requests_pathname_prefix=real_prefix,
routes_pathname_prefix="/_internal/",
# requests_pathname_prefix=requests_pathname_prefix,
meta_tags=[ meta_tags=[
{"name": "viewport", "content": "width=device-width, initial-scale=1"}, {"name": "viewport", "content": "width=device-width, initial-scale=1"},
], ],
@ -63,30 +75,8 @@ def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash:
# Register pages # Register pages
for path, page in pages.items(): for path, page in pages.items():
# dash.register_page(page["name"], path=path, layout=page["layout"])
page["blueprint"].register(app, path, prefix=page["prefix"]) page["blueprint"].register(app, path, prefix=page["prefix"])
# 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 # Create navbar
navbar = dbc.Navbar( navbar = dbc.Navbar(
dbc.Container( dbc.Container(
@ -104,8 +94,47 @@ def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash:
dark=True, dark=True,
) )
# Define app layout roles = Roles()
app.layout = html.Div(
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.Location(id="url", refresh=False),
dcc.Interval( dcc.Interval(
@ -121,7 +150,11 @@ def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash:
] ]
) )
setup_auth_clientcallback(app) return layout
app.layout = create_layout
setup_auth_clientcallbacks(app)
# Открытие на кнопку меню # Открытие на кнопку меню
app.clientside_callback( app.clientside_callback(