mirror of
https://gitflic.ru/project/maks1ms/ocab.git
synced 2025-11-28 10:21:55 +03:00
Compare commits
2 Commits
63b321a500
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8953b2a15 | ||
|
|
de9a954fe3 |
6
.flake8
6
.flake8
@@ -1,6 +0,0 @@
|
|||||||
[flake8]
|
|
||||||
per-file-ignores =
|
|
||||||
__init__.py:F401
|
|
||||||
max-line-length = 88
|
|
||||||
count = true
|
|
||||||
extend-ignore = E203,E701
|
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,10 +0,0 @@
|
|||||||
.idea
|
|
||||||
.vscode
|
|
||||||
.env
|
|
||||||
env
|
|
||||||
.venv
|
|
||||||
venv
|
|
||||||
__pycache__
|
|
||||||
OCAB.db
|
|
||||||
config.yaml
|
|
||||||
dist
|
|
||||||
7
.mailmap
7
.mailmap
@@ -1,7 +0,0 @@
|
|||||||
armatik <s.fomchenkov@yandex.ru> <57626821+armatik@users.noreply.github.com>
|
|
||||||
armatik <s.fomchenkov@yandex.ru> <57626821+Armatik@users.noreply.github.com>
|
|
||||||
armatik <s.fomchenkov@yandex.ru> Armatik <s.fomchenkov@yandex.ru>
|
|
||||||
|
|
||||||
ilyazheprog <ilyazheprog@gmail.com> <ilya_zhenetskij@vk.com>
|
|
||||||
|
|
||||||
Maxim Slipenko <maxim@slipenko.com> Максим Слипенко <maxim@slipenko.com>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# See https://pre-commit.com for more information
|
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/crashappsec/pre-commit-sync
|
|
||||||
rev: 04b0e02eefa7c41bedca7456ad542e60b67c16c6
|
|
||||||
hooks:
|
|
||||||
- id: pre-commit-sync
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v3.2.0
|
|
||||||
hooks:
|
|
||||||
- id: trailing-whitespace
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: check-yaml
|
|
||||||
- id: check-added-large-files
|
|
||||||
- repo: https://github.com/PyCQA/isort
|
|
||||||
rev: 5.13.2 # sync:isort:poetry.lock
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
- repo: https://github.com/psf/black
|
|
||||||
rev: 24.4.2 # sync:black:poetry.lock
|
|
||||||
hooks:
|
|
||||||
- id: black
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
|
||||||
rev: 7.1.0 # sync:flake8:poetry.lock
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
|
||||||
rev: 1.7.9 # sync:bandit:poetry.lock
|
|
||||||
hooks:
|
|
||||||
- id: bandit
|
|
||||||
61
README.md
61
README.md
@@ -1,60 +1 @@
|
|||||||
# OpenChatAiBot V2
|
# OpenChatAiBot
|
||||||
|
|
||||||
## Что такое OCAB?
|
|
||||||
|
|
||||||
OCAB - это бот для Telegram, который призван помочь во взаимодействии с чатом.
|
|
||||||
Бот поддерживает интеграцию модулей для расширения функционала.
|
|
||||||
Фактически бот является платформой для запуска созданных для него модулей.
|
|
||||||
Модули могут взаимодействовать друг с другом или быть полностью независимыми.
|
|
||||||
|
|
||||||
## Что такое модуль?
|
|
||||||
|
|
||||||
Модуль - это директория, которая содержит в себе код модуля и его конфигурацию.
|
|
||||||
|
|
||||||
### Структура модуля
|
|
||||||
|
|
||||||
*Будет дополнено после закрытия [issue #17](https://gitflic.ru/project/armatik/ocab/issue/17).*
|
|
||||||
|
|
||||||
## Стандартные модули
|
|
||||||
|
|
||||||
В стандартный состав бота входят следующие модули:
|
|
||||||
|
|
||||||
* `admin` - модуль для модерирования чата. Позволяет удалять сообщения, банить пользователей и т.д.
|
|
||||||
* `reputation` - модуль репутации пользователей. Позволяет оценивать ответы пользователей и накапливать репутацию.
|
|
||||||
* `welcome` - модуль приветствия новых пользователей. Позволяет приветствовать новых пользователей в чате, а также
|
|
||||||
проверять пользователя капчей для предотвращения спама.
|
|
||||||
* `roles` - модуль ролей. Позволяет назначать пользователям роли и ограничивать доступ к командам бота по ролям.
|
|
||||||
Является важной частью системы прав доступа и модуля `admin`.
|
|
||||||
|
|
||||||
## Дополнительные официальные модули
|
|
||||||
|
|
||||||
* `yandexgpt` - модуль для генерации ответов на основе нейросети GPT-3.5. Позволяет боту отвечать на сообщения
|
|
||||||
пользователей, используя нейросеть. Ключевой особенностью является построение линии контекста для нейросети,
|
|
||||||
которая позволяет боту отвечать на вопросы, используя контекст предыдущих сообщений. Для этого используется
|
|
||||||
модуль база данных хранящий историю сообщений.
|
|
||||||
<!--
|
|
||||||
* `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 для работы с базой данных.
|
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
# Спецификация модулей
|
|
||||||
|
|
||||||
> **Внимание!**
|
|
||||||
>
|
|
||||||
> Данная спецификация еще не закончена и активно разрабатывается.
|
|
||||||
> Могут быть значительные изменения (breaking changes).
|
|
||||||
|
|
||||||
Каждый модуль представлен в виде папки, содержащей два обязательных файла: info.json и `__init__.py`.
|
|
||||||
|
|
||||||
## Метаинформация о модуле (info.json)
|
|
||||||
|
|
||||||
Этот файл содержит метаинформацию о модуле в формате JSON. Пример структуры info.json приведен ниже:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "standard.info",
|
|
||||||
"name": "Info",
|
|
||||||
"description": "Модуль с информацией",
|
|
||||||
"author": "OCAB Team",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"privileged": false,
|
|
||||||
"dependencies": {
|
|
||||||
"required": {
|
|
||||||
"standard.roles": "^1.0.0",
|
|
||||||
"standard.database": {
|
|
||||||
"version": "^1.0.0",
|
|
||||||
"uses": [
|
|
||||||
"db_api"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"optional": {
|
|
||||||
"external.yandexgpt": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pythonDependencies": {
|
|
||||||
"required": {
|
|
||||||
"some_package": "^1.2.3"
|
|
||||||
},
|
|
||||||
"optional": {
|
|
||||||
"another_package": "*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `id`: Уникальный идентификатор модуля.
|
|
||||||
- `name`: Название модуля.
|
|
||||||
- `description`: Описание функциональности модуля.
|
|
||||||
- `author`: Автор модуля.
|
|
||||||
- `version`: Версия модуля в формате [SemVer](https://semver.org/).
|
|
||||||
- `privileged`: Булево значение, указывающее, является ли модуль привилегированным.
|
|
||||||
- `dependencies`: Объект, описывающий зависимости модуля от других **OCAB** модулей.
|
|
||||||
- `required`: Обязательные зависимости. Ключ - идентификатор модуля, значение - версия или объект `DependencyInfo`.
|
|
||||||
- `optional`: Необязательные зависимости. Ключ - идентификатор модуля, значение - версия или объект `DependencyInfo`.
|
|
||||||
- `pythonDependencies`: Объект, описывающий зависимости модуля от внешних Python пакетов.
|
|
||||||
- `required`: Обязательные зависимости. Ключ - название пакета, значение - версия.
|
|
||||||
- `optional`: Необязательные зависимости. Ключ - название пакета, значение - версия.
|
|
||||||
|
|
||||||
### DependencyInfo
|
|
||||||
|
|
||||||
Объект `DependencyInfo` позволяет указать не только версию зависимости, но и список разрешенных к использованию
|
|
||||||
атрибутов модуля (`uses`). Если `uses` не указан, то доступ к модулю целиком запрещен.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "^1.0.0",
|
|
||||||
"uses": [
|
|
||||||
"db_api"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `version`: Версия модуля.
|
|
||||||
- `uses`: Список разрешенных атрибутов модуля.
|
|
||||||
|
|
||||||
## Режимы выполнения модулей
|
|
||||||
|
|
||||||
**Непривилегированный режим** (`privileged: false`):
|
|
||||||
- Модуль выполняется в доверенной среде на основе RestrictedPython (это накладывает ряд ограничений).
|
|
||||||
- Может импортировать только явно разрешенные модули, указанные в `pythonDependencies`,
|
|
||||||
а также несколько стандартных модулей, необходимых для работы.
|
|
||||||
- Имеет доступ к пакету `ocab_core.modules_system.public_api` для взаимодействия с ботом.
|
|
||||||
|
|
||||||
**Привилегированный режим** (`privileged: true`):
|
|
||||||
- Модуль выполняется без ограничений.
|
|
||||||
- Имеет полный доступ ко всем пакетам, доступным в окружении.
|
|
||||||
- Должен использоваться с осторожностью и только для модулей, требующих расширенных прав.
|
|
||||||
|
|
||||||
## Жизненный цикл модуля
|
|
||||||
|
|
||||||
1. Загрузка метаданных из `info.json`.
|
|
||||||
2. Проверка зависимостей:
|
|
||||||
- Проверяется наличие всех обязательных зависимостей.
|
|
||||||
- Проверяется совместимость версий зависимостей.
|
|
||||||
- Проверяется наличие Python зависимостей.
|
|
||||||
3. Загрузка кода модуля из `__init__.py`.
|
|
||||||
4. Вызов функции `module_init` (если она есть).
|
|
||||||
5. После загрузки всех модулей вызывается функция `module_late_init` (если она есть).
|
|
||||||
|
|
||||||
## Взаимодействие между модулями
|
|
||||||
|
|
||||||
Модули могут взаимодействовать друг с другом через [API](../src/ocab_core/ocab_core/modules_system/public_api/__init__.py),
|
|
||||||
предоставляемое системой управления модулями.
|
|
||||||
|
|
||||||
Например, есть функция `get_module`. Она позволяет получить модуль или предоставляемые им объекты по его
|
|
||||||
идентификатору.
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"name": "OCAB Monorepo Root",
|
|
||||||
"path": ".",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "OCAB Modules",
|
|
||||||
"path": "src/ocab_modules"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "OCAB Core",
|
|
||||||
"path": "src/ocab_core"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Gnomik",
|
|
||||||
"path": "src/gnomik"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extensions": {
|
|
||||||
"recommendations": [
|
|
||||||
"ms-python.python"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
475
poetry.lock
generated
475
poetry.lock
generated
@@ -1,475 +0,0 @@
|
|||||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bandit"
|
|
||||||
version = "1.7.9"
|
|
||||||
description = "Security oriented static analyser for python code."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"},
|
|
||||||
{file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
|
|
||||||
PyYAML = ">=5.3.1"
|
|
||||||
rich = "*"
|
|
||||||
stevedore = ">=1.20.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
baseline = ["GitPython (>=3.1.30)"]
|
|
||||||
sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"]
|
|
||||||
test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"]
|
|
||||||
toml = ["tomli (>=1.1.0)"]
|
|
||||||
yaml = ["PyYAML"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "black"
|
|
||||||
version = "24.4.2"
|
|
||||||
description = "The uncompromising code formatter."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
|
|
||||||
{file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
|
|
||||||
{file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"},
|
|
||||||
{file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"},
|
|
||||||
{file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"},
|
|
||||||
{file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"},
|
|
||||||
{file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"},
|
|
||||||
{file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"},
|
|
||||||
{file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"},
|
|
||||||
{file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"},
|
|
||||||
{file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"},
|
|
||||||
{file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"},
|
|
||||||
{file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"},
|
|
||||||
{file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"},
|
|
||||||
{file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"},
|
|
||||||
{file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"},
|
|
||||||
{file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"},
|
|
||||||
{file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"},
|
|
||||||
{file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"},
|
|
||||||
{file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"},
|
|
||||||
{file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"},
|
|
||||||
{file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
click = ">=8.0.0"
|
|
||||||
mypy-extensions = ">=0.4.3"
|
|
||||||
packaging = ">=22.0"
|
|
||||||
pathspec = ">=0.9.0"
|
|
||||||
platformdirs = ">=2"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
colorama = ["colorama (>=0.4.3)"]
|
|
||||||
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
|
|
||||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
|
||||||
uvloop = ["uvloop (>=0.15.2)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfgv"
|
|
||||||
version = "3.4.0"
|
|
||||||
description = "Validate configuration and produce human readable error messages."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
|
|
||||||
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "click"
|
|
||||||
version = "8.1.7"
|
|
||||||
description = "Composable command line interface toolkit"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
|
||||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorama"
|
|
||||||
version = "0.4.6"
|
|
||||||
description = "Cross-platform colored terminal text."
|
|
||||||
optional = false
|
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
|
||||||
files = [
|
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "distlib"
|
|
||||||
version = "0.3.8"
|
|
||||||
description = "Distribution utilities"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
|
|
||||||
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "filelock"
|
|
||||||
version = "3.15.4"
|
|
||||||
description = "A platform independent file lock."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
|
|
||||||
{file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
|
|
||||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
|
|
||||||
typing = ["typing-extensions (>=4.8)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "flake8"
|
|
||||||
version = "7.1.0"
|
|
||||||
description = "the modular source code checker: pep8 pyflakes and co"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8.1"
|
|
||||||
files = [
|
|
||||||
{file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"},
|
|
||||||
{file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
mccabe = ">=0.7.0,<0.8.0"
|
|
||||||
pycodestyle = ">=2.12.0,<2.13.0"
|
|
||||||
pyflakes = ">=3.2.0,<3.3.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "identify"
|
|
||||||
version = "2.6.0"
|
|
||||||
description = "File identification library for Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
|
|
||||||
{file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
license = ["ukkonen"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "isort"
|
|
||||||
version = "5.13.2"
|
|
||||||
description = "A Python utility / library to sort Python imports."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8.0"
|
|
||||||
files = [
|
|
||||||
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
|
|
||||||
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
colors = ["colorama (>=0.4.6)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "markdown-it-py"
|
|
||||||
version = "3.0.0"
|
|
||||||
description = "Python port of markdown-it. Markdown parsing, done right!"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
|
|
||||||
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
mdurl = ">=0.1,<1.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
|
|
||||||
code-style = ["pre-commit (>=3.0,<4.0)"]
|
|
||||||
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
|
|
||||||
linkify = ["linkify-it-py (>=1,<3)"]
|
|
||||||
plugins = ["mdit-py-plugins"]
|
|
||||||
profiling = ["gprof2dot"]
|
|
||||||
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
|
|
||||||
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mccabe"
|
|
||||||
version = "0.7.0"
|
|
||||||
description = "McCabe checker, plugin for flake8"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
files = [
|
|
||||||
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
|
||||||
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mdurl"
|
|
||||||
version = "0.1.2"
|
|
||||||
description = "Markdown URL utilities"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
|
||||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy-extensions"
|
|
||||||
version = "1.0.0"
|
|
||||||
description = "Type system extensions for programs checked with the mypy type checker."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5"
|
|
||||||
files = [
|
|
||||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
|
||||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nodeenv"
|
|
||||||
version = "1.9.1"
|
|
||||||
description = "Node.js virtual environment builder"
|
|
||||||
optional = false
|
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
|
||||||
files = [
|
|
||||||
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
|
|
||||||
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "packaging"
|
|
||||||
version = "24.1"
|
|
||||||
description = "Core utilities for Python packages"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
|
||||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pathspec"
|
|
||||||
version = "0.12.1"
|
|
||||||
description = "Utility library for gitignore style pattern matching of file paths."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
|
||||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pbr"
|
|
||||||
version = "6.0.0"
|
|
||||||
description = "Python Build Reasonableness"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.6"
|
|
||||||
files = [
|
|
||||||
{file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"},
|
|
||||||
{file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "platformdirs"
|
|
||||||
version = "4.2.2"
|
|
||||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
|
|
||||||
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
|
|
||||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
|
|
||||||
type = ["mypy (>=1.8)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pre-commit"
|
|
||||||
version = "3.7.1"
|
|
||||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.9"
|
|
||||||
files = [
|
|
||||||
{file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"},
|
|
||||||
{file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
cfgv = ">=2.0.0"
|
|
||||||
identify = ">=1.0.0"
|
|
||||||
nodeenv = ">=0.11.1"
|
|
||||||
pyyaml = ">=5.1"
|
|
||||||
virtualenv = ">=20.10.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pycodestyle"
|
|
||||||
version = "2.12.0"
|
|
||||||
description = "Python style guide checker"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"},
|
|
||||||
{file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyflakes"
|
|
||||||
version = "3.2.0"
|
|
||||||
description = "passive checker of Python programs"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"},
|
|
||||||
{file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pygments"
|
|
||||||
version = "2.18.0"
|
|
||||||
description = "Pygments is a syntax highlighting package written in Python."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
|
|
||||||
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
windows-terminal = ["colorama (>=0.4.6)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyyaml"
|
|
||||||
version = "6.0.1"
|
|
||||||
description = "YAML parser and emitter for Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
files = [
|
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
|
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
|
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
|
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
|
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
|
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
|
|
||||||
{file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
|
|
||||||
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
|
|
||||||
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
|
|
||||||
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
|
|
||||||
{file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
|
|
||||||
{file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
|
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
|
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
|
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
|
||||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rich"
|
|
||||||
version = "13.7.1"
|
|
||||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7.0"
|
|
||||||
files = [
|
|
||||||
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
|
|
||||||
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
markdown-it-py = ">=2.2.0"
|
|
||||||
pygments = ">=2.13.0,<3.0.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver"
|
|
||||||
version = "3.0.2"
|
|
||||||
description = "Python helper for Semantic Versioning (https://semver.org)"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"},
|
|
||||||
{file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "stevedore"
|
|
||||||
version = "5.2.0"
|
|
||||||
description = "Manage dynamic plugins for Python applications"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"},
|
|
||||||
{file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
pbr = ">=2.0.0,<2.1.0 || >2.1.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "virtualenv"
|
|
||||||
version = "20.26.3"
|
|
||||||
description = "Virtual Python Environment builder"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"},
|
|
||||||
{file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
distlib = ">=0.3.7,<1"
|
|
||||||
filelock = ">=3.12.2,<4"
|
|
||||||
platformdirs = ">=3.9.1,<5"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
|
||||||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
lock-version = "2.0"
|
|
||||||
python-versions = "~3.12"
|
|
||||||
content-hash = "7db42302f410e90c105f188f32680864d9268137049af500f4221e69b4f9cc7f"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[virtualenvs]
|
|
||||||
in-project = true
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
[tool.poetry]
|
|
||||||
name = "ocab-monorepo"
|
|
||||||
version = "2.0.0"
|
|
||||||
description = "OCAB is a modular Telegram bot"
|
|
||||||
license = "GPL-3.0-only"
|
|
||||||
authors = ["Семён Фомченков <s.fomchenkov@yandex.ru>"]
|
|
||||||
maintainers = [
|
|
||||||
"Илья Женецкий <ilya_zhenetskij@vk.com>",
|
|
||||||
"qualimock <qualimock@yandex.ru>",
|
|
||||||
"Кирилл Уницаев <fiersik.kouji@yandex.ru>",
|
|
||||||
"Максим Слипенко <maxim@slipenko.com>"
|
|
||||||
]
|
|
||||||
readme = "README.md"
|
|
||||||
repository = "https://gitflic.ru/project/armatik/ocab"
|
|
||||||
packages = [
|
|
||||||
{ include = "scripts" }
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.poetry.urls]
|
|
||||||
"Bug Tracker" = "https://gitflic.ru/project/armatik/ocab/issue?status=OPEN"
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
|
||||||
test = 'scripts.test:main'
|
|
||||||
init = 'scripts.init:main'
|
|
||||||
module = 'scripts.module:main'
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "~3.12"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
flake8 = "^7.1.0"
|
|
||||||
black = "^24.4.2"
|
|
||||||
isort = "^5.13.2"
|
|
||||||
bandit = "^1.7.9"
|
|
||||||
pre-commit = "^3.7.1"
|
|
||||||
semver = "^3.0.2"
|
|
||||||
|
|
||||||
[tool.black]
|
|
||||||
line-length = 88
|
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
profile = "black"
|
|
||||||
line_length = 88
|
|
||||||
multi_line_output = 3
|
|
||||||
skip_gitignore = true
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
aiogram==3.1.1
|
||||||
|
openai==0.27.8
|
||||||
|
PyYAML==6.0.1
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
from json import dumps
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
pwd = Path().cwd()
|
|
||||||
dir_core = pwd / "src" / "ocab_core"
|
|
||||||
dir_modules_standard = pwd / "src" / "ocab_modules" / "standard"
|
|
||||||
dir_modules_external = pwd / "src" / "ocab_modules" / "external"
|
|
||||||
|
|
||||||
json = {
|
|
||||||
"core": str(dir_core),
|
|
||||||
"modules standard": str(dir_modules_standard),
|
|
||||||
"modules external": str(dir_modules_external),
|
|
||||||
}
|
|
||||||
with open("src/paths.json", "w", encoding="utf8") as f:
|
|
||||||
f.write(dumps(json, indent=4))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
DEFAULTS = {
|
|
||||||
"description": "Очень полезный модуль",
|
|
||||||
"author": "OCAB Team",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"privileged": "false",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def create_module(args):
|
|
||||||
module_dir = os.path.join("src/ocab_modules/standard", args.module_name)
|
|
||||||
os.makedirs(module_dir, exist_ok=True)
|
|
||||||
|
|
||||||
module_info = {
|
|
||||||
"id": args.id,
|
|
||||||
"name": args.name,
|
|
||||||
"description": args.description,
|
|
||||||
"author": args.author,
|
|
||||||
"version": args.version,
|
|
||||||
"privileged": args.privileged.lower() == "true",
|
|
||||||
"dependencies": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
with open(os.path.join(module_dir, "info.json"), "w", encoding="utf-8") as f:
|
|
||||||
json.dump(module_info, f, ensure_ascii=False, indent=4)
|
|
||||||
|
|
||||||
with open(os.path.join(module_dir, "__init__.py"), "w", encoding="utf-8") as f:
|
|
||||||
f.write("# Init file for the module\n")
|
|
||||||
|
|
||||||
print(f"Module {args.module_name} created successfully.")
|
|
||||||
|
|
||||||
|
|
||||||
def interactive_mode(args):
|
|
||||||
def get_input(prompt, default=None):
|
|
||||||
if default:
|
|
||||||
value = input(f"{prompt} [{default}]: ")
|
|
||||||
return value if value else default
|
|
||||||
else:
|
|
||||||
value = input(f"{prompt}: ")
|
|
||||||
return value
|
|
||||||
|
|
||||||
module_name = get_input("Введите название модуля (папки)")
|
|
||||||
module_id = get_input("Введите ID")
|
|
||||||
name = get_input("Введите название модуля")
|
|
||||||
description = get_input(
|
|
||||||
"Введите описание модуля", args.description or DEFAULTS["description"]
|
|
||||||
)
|
|
||||||
author = get_input("Введите автора", args.author or DEFAULTS["author"])
|
|
||||||
version = get_input("Введите версию", args.version or DEFAULTS["version"])
|
|
||||||
privileged = get_input(
|
|
||||||
"Модуль привилегированный (true/false)",
|
|
||||||
args.privileged or DEFAULTS["privileged"],
|
|
||||||
)
|
|
||||||
|
|
||||||
args = argparse.Namespace(
|
|
||||||
command="create",
|
|
||||||
module_name=module_name,
|
|
||||||
id=module_id,
|
|
||||||
name=name,
|
|
||||||
description=description,
|
|
||||||
author=author,
|
|
||||||
version=version,
|
|
||||||
privileged=privileged,
|
|
||||||
dependencies="",
|
|
||||||
)
|
|
||||||
|
|
||||||
create_module(args)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Утилита для создания директории модуля с файлами."
|
|
||||||
)
|
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
||||||
|
|
||||||
create_parser = subparsers.add_parser("create", help="Создать новый модуль")
|
|
||||||
create_parser.add_argument("--module_name", help="Название директории модуля")
|
|
||||||
create_parser.add_argument("--id", help="ID модуля")
|
|
||||||
create_parser.add_argument("--name", help="Название модуля")
|
|
||||||
create_parser.add_argument("--description", help="Описание модуля")
|
|
||||||
create_parser.add_argument("--author", help="Автор модуля")
|
|
||||||
create_parser.add_argument("--version", help="Версия модуля")
|
|
||||||
create_parser.add_argument(
|
|
||||||
"--privileged", help="Привилегированный модуль (true/false)"
|
|
||||||
)
|
|
||||||
create_parser.add_argument(
|
|
||||||
"--dependencies", help="Список зависимостей в формате имя:версия через запятую"
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.command == "create":
|
|
||||||
if not all(
|
|
||||||
[
|
|
||||||
args.module_name,
|
|
||||||
args.id,
|
|
||||||
args.name,
|
|
||||||
args.description,
|
|
||||||
args.author,
|
|
||||||
args.version,
|
|
||||||
args.privileged,
|
|
||||||
args.dependencies,
|
|
||||||
]
|
|
||||||
):
|
|
||||||
print("Переход в интерактивный режим...")
|
|
||||||
interactive_mode(args)
|
|
||||||
else:
|
|
||||||
create_module(args)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import subprocess # nosec
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
subprocess.run(["python", "-u", "-m", "unittest", "discover"]) # nosec
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
198
src/OpenAI/GPT35turbo/OA_processing.py
Normal file
198
src/OpenAI/GPT35turbo/OA_processing.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import configparser
|
||||||
|
from openai import OpenAIError
|
||||||
|
import time
|
||||||
|
|
||||||
|
mother_path = os.path.dirname(os.path.dirname(os.getcwd()))
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(os.path.join(mother_path, 'src/config.ini'))
|
||||||
|
|
||||||
|
database = sqlite3.connect(os.path.join(mother_path, 'DataBase/OCAB_DB.db'))
|
||||||
|
cursor = database.cursor()
|
||||||
|
reply_ignore = config['Telegram']['reply_ignore'].split('| ')
|
||||||
|
reply_ignore = list(map(int, reply_ignore))
|
||||||
|
#print(reply_ignore)
|
||||||
|
|
||||||
|
min_token_for_answer = int(config['Openai']['min_token_for_answer'])
|
||||||
|
|
||||||
|
# Импорт библиотек
|
||||||
|
|
||||||
|
import openai
|
||||||
|
max_token_count = int(config['Openai']['max_token_count'])
|
||||||
|
|
||||||
|
# Создание файла лога если его нет
|
||||||
|
if not os.path.exists(os.path.join(mother_path, 'src/OpenAI/GPT35turbo/log.txt')):
|
||||||
|
with open(os.path.join(mother_path, 'src/OpenAI/GPT35turbo/log.txt'), 'w') as log_file:
|
||||||
|
log_file.write('')
|
||||||
|
|
||||||
|
|
||||||
|
def openai_response(message_formated_text):
|
||||||
|
# Запуск OpenAI
|
||||||
|
# Считаем размер полученного текста
|
||||||
|
#print(message_formated_text)
|
||||||
|
count_length = 0
|
||||||
|
if len(message_formated_text) == 0:
|
||||||
|
message_formated_text = [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Напиши короткий ответ говорящий что контекст сообщения слишком длинный и попроси задать вопрос отдельно без ответа на другие сообщения по ключевому слову"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for message in message_formated_text:
|
||||||
|
#print(message["content"])
|
||||||
|
count_length += int(len(message["content"]))
|
||||||
|
#print(count_length)
|
||||||
|
try:
|
||||||
|
response = openai.ChatCompletion.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=message_formated_text,
|
||||||
|
max_tokens=max_token_count - count_length
|
||||||
|
)
|
||||||
|
except OpenAIError as ex:
|
||||||
|
if 'on requests per min. Limit: 3 / min. Please try again' in str(ex):
|
||||||
|
response = ('Извини мой процессор перегрелся, дай мне минутку отдохнуть')
|
||||||
|
elif 'Bad gateway.' in str(ex):
|
||||||
|
response = (
|
||||||
|
'Ой, где я? Кажется кто то перерзал мой интернет кабель, подожди немного пока я его починю')
|
||||||
|
#запись ошибки в лог с указанием времени и даты
|
||||||
|
with open(os.path.join(mother_path, 'src/OpenAI/GPT35turbo/log.txt'), 'a') as log_file:
|
||||||
|
log_file.write('\n' + time.strftime("%d.%m.%Y %H:%M:%S") + ' ' + str(ex))
|
||||||
|
return response
|
||||||
|
|
||||||
|
def sort_message_from_user(message_formated_text, message_id):
|
||||||
|
# print(int(*(
|
||||||
|
# cursor.execute("SELECT message_sender FROM message_list WHERE message_id = ?", (message_id,)).fetchone())))
|
||||||
|
if int(*(
|
||||||
|
cursor.execute("SELECT message_sender FROM message_list WHERE message_id = ?",
|
||||||
|
(message_id,)).fetchone())) == 0:
|
||||||
|
message_formated_text.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": str(*(cursor.execute("SELECT message_text FROM message_list WHERE message_id = ?",
|
||||||
|
(message_id,)).fetchone()))
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
message_formated_text.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": str(*(cursor.execute("SELECT message_text FROM message_list WHERE message_id = ?",
|
||||||
|
(message_id,)).fetchone()))
|
||||||
|
})
|
||||||
|
#Проверка что длина всех сообщений в кортеже не превышает max_token_count-min_token_for_answer
|
||||||
|
return message_formated_text
|
||||||
|
|
||||||
|
def openai_collecting_message(message_id, message_formated_text):
|
||||||
|
# собирает цепочку сообщений для OpenAI длинной до max_token_count
|
||||||
|
# проверяем что сообщение отвечает на другое сообщение
|
||||||
|
#print(int(*(cursor.execute("SELECT answer_id FROM message_list WHERE message_id = ?", (message_id,)).fetchone())))
|
||||||
|
#print(reply_ignore)
|
||||||
|
if int(*(cursor.execute("SELECT answer_id FROM message_list WHERE message_id = ?", (message_id,)).fetchone())) not in reply_ignore:
|
||||||
|
# Продолжаем искать ответы на сообщения
|
||||||
|
#print(int(*(cursor.execute("SELECT answer_id FROM message_list WHERE message_id = ?", (message_id,)).fetchone())))
|
||||||
|
message_formated_text = openai_collecting_message(int(*(cursor.execute("SELECT answer_id FROM message_list WHERE message_id = ?", (message_id,)).fetchone())), message_formated_text)
|
||||||
|
#Проверяем ID отправителя сообщения, если 0 то это сообщение от бота
|
||||||
|
sort_message_from_user(message_formated_text, message_id)
|
||||||
|
else:
|
||||||
|
# Проверяем ID отправителя сообщения, если 0 то это сообщение от бота
|
||||||
|
sort_message_from_user(message_formated_text, message_id)
|
||||||
|
return message_formated_text
|
||||||
|
|
||||||
|
|
||||||
|
def openai_message_processing(message_id):
|
||||||
|
#проверяем на наличие сообщения в базе данных
|
||||||
|
if cursor.execute("SELECT message_text FROM message_list WHERE message_id = ?", (message_id,)).fetchone() is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
# проверяем на то что сообщение влезает в max_token_count с учётом message_formated_text
|
||||||
|
message_formated_text = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": config['Openai']['story_model']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if ((len(str(cursor.execute("SELECT message_text FROM message_list WHERE message_id")))) < (max_token_count - len(message_formated_text[0]['content']))):
|
||||||
|
message_formated_text = openai_collecting_message(message_id, message_formated_text)
|
||||||
|
count_length = 0
|
||||||
|
# Обработка невозможности ответить на сообщение
|
||||||
|
try:
|
||||||
|
for message in message_formated_text:
|
||||||
|
count_length += len(message['content'])
|
||||||
|
while count_length > max_token_count - min_token_for_answer:
|
||||||
|
message_formated_text.pop(1)
|
||||||
|
count_length = 0
|
||||||
|
for message in message_formated_text:
|
||||||
|
count_length += len(message['content'])
|
||||||
|
except IndexError:
|
||||||
|
message_formated_text = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "Выведи сообщение об ошибке."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
response = openai_response(message_formated_text)
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
return f"Сообщение слишком длинное, максимальная длина сообщения \
|
||||||
|
{max_token_count - len(message_formated_text[0]['content'])} символов, укоротите его на \
|
||||||
|
{len(str(cursor.execute('SELECT message_text FROM message_list WHERE message_id'))) - max_token_count} символов"
|
||||||
|
|
||||||
|
def openai_collecting_history_context(start_id, end_id, message_formated_text, message_id = 0):
|
||||||
|
# собираем список сообщений для OpenAI длинной до 14500 символов начиная с end_id и заканчивая start_id
|
||||||
|
if message_id == 0:
|
||||||
|
message_id = end_id
|
||||||
|
message_formated_text = openai_collecting_history_context(start_id, end_id, message_formated_text, message_id)
|
||||||
|
elif message_id > start_id:
|
||||||
|
message_formated_text = openai_collecting_history_context(start_id, end_id, message_formated_text, (message_id - 1))
|
||||||
|
try:
|
||||||
|
message_formated_text.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": str(*(cursor.execute("SELECT message_text FROM message_list WHERE message_id = ?",
|
||||||
|
(message_id,)).fetchone()))
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return message_formated_text
|
||||||
|
|
||||||
|
def openai_message_history_processing(start_id, end_id):
|
||||||
|
message_formated_text = []
|
||||||
|
message_formated_text = openai_collecting_history_context(start_id, end_id, message_formated_text)
|
||||||
|
|
||||||
|
max_token_count_history = 15000
|
||||||
|
min_token_for_answer_history = 1500
|
||||||
|
count_length = 0
|
||||||
|
try:
|
||||||
|
for message in message_formated_text:
|
||||||
|
count_length += len(message['content'])
|
||||||
|
while count_length > max_token_count_history - min_token_for_answer_history:
|
||||||
|
message_formated_text.pop(1)
|
||||||
|
count_length = 0
|
||||||
|
for message in message_formated_text:
|
||||||
|
count_length += len(message['content'])
|
||||||
|
except IndexError:
|
||||||
|
message_formated_text = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "Выведи сообщение об ошибке."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
message_formated_text.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": "Сделай пересказ всей истории сообщений без потери смысла от 3-его лица."
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
response = openai.ChatCompletion.create(
|
||||||
|
model="gpt-3.5-turbo-16k",
|
||||||
|
messages=message_formated_text,
|
||||||
|
max_tokens=max_token_count_history - count_length
|
||||||
|
)
|
||||||
|
except OpenAIError as ex:
|
||||||
|
if 'on requests per min. Limit: 3 / min. Please try again' in str(ex):
|
||||||
|
response = ('Извини мой процессор перегрелся, дай мне минутку отдохнуть')
|
||||||
|
elif 'Bad gateway.' in str(ex):
|
||||||
|
response = (
|
||||||
|
'Ой, где я? Кажется кто то перерзал мой интернет кабель, подожди немного пока я его починю')
|
||||||
|
# запись ошибки в лог с указанием времени и даты
|
||||||
|
with open(os.path.join(mother_path, 'src/OpenAI/GPT35turbo/log.txt'), 'a') as log_file:
|
||||||
|
log_file.write('\n' + time.strftime("%d.%m.%Y %H:%M:%S") + ' ' + str(ex))
|
||||||
|
return response
|
||||||
8
src/TelegramBot/MessageHandler.py
Normal file
8
src/TelegramBot/MessageHandler.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Получение сообщений в чате, и запись их в базу данных
|
||||||
|
from aiogram import types
|
||||||
|
|
||||||
|
from src.TelegramBot.main import dp
|
||||||
|
|
||||||
|
from main import cursor, config
|
||||||
|
|
||||||
|
#импортировать функцию для обработки сообщений из OpenAI
|
||||||
566
src/TelegramBot/main.py
Normal file
566
src/TelegramBot/main.py
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
# Импорт библиотек
|
||||||
|
import os
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import openai
|
||||||
|
import configparser
|
||||||
|
import sqlite3
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import datetime
|
||||||
|
from time import mktime
|
||||||
|
|
||||||
|
from aiogram import Bot, Dispatcher, executor, types
|
||||||
|
from aiogram.contrib.fsm_storage.memory import MemoryStorage
|
||||||
|
from aiogram.utils.exceptions import MessageCantBeDeleted, MessageToDeleteNotFound
|
||||||
|
|
||||||
|
mother_path = os.path.dirname(os.path.dirname(os.getcwd()))
|
||||||
|
sys.path.insert(1, mother_path)
|
||||||
|
|
||||||
|
|
||||||
|
# Импорт переменных из файла .ini
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(os.path.join(mother_path, 'src/config.ini'))
|
||||||
|
TOKEN = config['Telegram']['token']
|
||||||
|
OPENAI_API_KEY = (config['Openai']['api_key'])
|
||||||
|
bot_trigger_front = (config['Telegram']['bot_trigger_front']).split('|')
|
||||||
|
bot_trigger_all = (config['Telegram']['bot_trigger_all']).split('|')
|
||||||
|
# удаление лишних элементов массивов
|
||||||
|
bot_trigger_front.remove('')
|
||||||
|
bot_trigger_all.remove('')
|
||||||
|
DB_message_limit = int(config['DataBase']['message_limit'])
|
||||||
|
|
||||||
|
# Инициализация бота
|
||||||
|
|
||||||
|
bot = Bot(token=TOKEN)
|
||||||
|
dp = Dispatcher(bot, storage=MemoryStorage())
|
||||||
|
|
||||||
|
# Инициализация API OpenAI
|
||||||
|
|
||||||
|
openai.api_key = OPENAI_API_KEY
|
||||||
|
|
||||||
|
# Инициализация базы данных OCAB_DB в папке DataBase/OCAB_DB.db
|
||||||
|
# Создаём базу данных sqlite3 по пути /home/armatik/PycharmProjects/OpenChatAiBot/DataBase/OCAB_DB.db
|
||||||
|
database = sqlite3.connect(os.path.join(mother_path, 'DataBase/OCAB_DB.db'))
|
||||||
|
cursor = database.cursor()
|
||||||
|
# Создаём таблицу chat_list
|
||||||
|
cursor.execute("""CREATE TABLE IF NOT EXISTS chat_list (
|
||||||
|
chat_id INTEGER PRIMARY KEY,
|
||||||
|
chat_role INTEGER NOT NULL,
|
||||||
|
chat_stats INTEGER NOT NULL
|
||||||
|
)""")
|
||||||
|
# Создаём таблицу message_list
|
||||||
|
cursor.execute("""CREATE TABLE IF NOT EXISTS message_list (
|
||||||
|
message_id INTEGER PRIMARY KEY,
|
||||||
|
message_text TEXT NOT NULL,
|
||||||
|
message_sender INTEGER NOT NULL,
|
||||||
|
answer_id INTEGER
|
||||||
|
)""")
|
||||||
|
# Создаём таблицу user_list
|
||||||
|
cursor.execute("""CREATE TABLE IF NOT EXISTS user_list (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
user_name TEXT NOT NULL,
|
||||||
|
user_role INTEGER,
|
||||||
|
user_stats INTEGER
|
||||||
|
user_rep INTEGER
|
||||||
|
)""")
|
||||||
|
#запись информации о чате в базу данных
|
||||||
|
|
||||||
|
async def empty_role(id):
|
||||||
|
cursor.execute("UPDATE user_list SET user_role = ? WHERE user_id = ?", (0, id))
|
||||||
|
database.commit()
|
||||||
|
|
||||||
|
async def check(id):
|
||||||
|
#проверка что у человека есть роль
|
||||||
|
user_role = cursor.execute("SELECT user_role FROM user_list WHERE user_id = ?", (id,)).fetchone()[0]
|
||||||
|
if user_role not in [0, 1, 2]:
|
||||||
|
await empty_role(id)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_role_name(rolenum):
|
||||||
|
rolenum = int(rolenum)
|
||||||
|
if rolenum == 0:
|
||||||
|
role = config['Roles']['user']
|
||||||
|
elif rolenum == 1:
|
||||||
|
role = config['Roles']['moderator']
|
||||||
|
elif rolenum == 2:
|
||||||
|
role = config['Roles']['admin']
|
||||||
|
return role
|
||||||
|
|
||||||
|
async def get_role(id):
|
||||||
|
#получение роли пользователя
|
||||||
|
user_role = cursor.execute("SELECT user_role FROM user_list WHERE user_id = ?", (id,)).fetchone()[0]
|
||||||
|
return await get_role_name(user_role)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_admin(id, chat_id):
|
||||||
|
#Проверка что человек есть в списке администраторов чата
|
||||||
|
chat_admins = await bot.get_chat_administrators(chat_id)
|
||||||
|
flag = False
|
||||||
|
for admin in chat_admins:
|
||||||
|
if admin.user.id == id:
|
||||||
|
flag = True
|
||||||
|
return flag
|
||||||
|
|
||||||
|
|
||||||
|
async def check_moderator(id):
|
||||||
|
#Проверка что человек имеет роль модератора
|
||||||
|
if cursor.execute("SELECT user_role FROM user_list WHERE user_id = ?", (id,)).fetchone()[0] >= 1:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def check_user(id):
|
||||||
|
#Проверка что человек имеет роль пользователя
|
||||||
|
if cursor.execute("SELECT user_role FROM user_list WHERE user_id = ?", (id,)).fetchone()[0] == 0:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def save_message(message):
|
||||||
|
#Сохранение сообщения в базу данных
|
||||||
|
cursor.execute("INSERT INTO message_list VALUES (?, ?, ?, ?)", (message.message_id, message.text, message.from_user.id, 0))
|
||||||
|
if message.reply_to_message is not None:
|
||||||
|
cursor.execute("UPDATE message_list SET answer_id = ? WHERE message_id = ?", (message.reply_to_message.message_id, message.message_id))
|
||||||
|
#Если отправитель не зарегистрирован в базе данных, то добавляем его
|
||||||
|
if cursor.execute("SELECT user_id FROM user_list WHERE user_id = ?", (message.from_user.id,)).fetchone() is None:
|
||||||
|
cursor.execute("INSERT INTO user_list VALUES (?, ?, ?, ?)", (message.from_user.id, message.from_user.username, 0, 1))
|
||||||
|
#Добавляем статистику в чат и в данные пользователя, если пользователь не бот.
|
||||||
|
cursor.execute("UPDATE chat_list SET chat_stats = chat_stats + 1 WHERE chat_id = ?", (message.chat.id,))
|
||||||
|
cursor.execute("UPDATE user_list SET user_stats = user_stats + 1 WHERE user_id = ?", (message.from_user.id,))
|
||||||
|
database.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def time_to_seconds(time):
|
||||||
|
#Конвертация текстового указания времени по типу 3h, 5m, 10s в минуты
|
||||||
|
if time[-1] == 'd':
|
||||||
|
return int(time[:-1])*86400
|
||||||
|
elif time[-1] == 'h':
|
||||||
|
return int(time[:-1])*3600
|
||||||
|
elif time[-1] == 'm':
|
||||||
|
return int(time[:-1])*60
|
||||||
|
elif time[-1] == 's':
|
||||||
|
return int(time[:-1])
|
||||||
|
|
||||||
|
|
||||||
|
async def short_time_to_time(time):
|
||||||
|
#Конвертация времени в длинное название
|
||||||
|
if time[-1] == 'd':
|
||||||
|
return str(f"{time[0:-1]} дней")
|
||||||
|
elif time[-1] == 'h':
|
||||||
|
return str(f"{time[0:-1]} часов")
|
||||||
|
elif time[-1] == 'm':
|
||||||
|
return str(f"{time[0:-1]} минут")
|
||||||
|
elif time[-1] == 's':
|
||||||
|
return str(f"{time[0:-1]} секунд")
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message_handler(commands=['mute'])
|
||||||
|
async def mute(message: types.Message):
|
||||||
|
#Проверка что отправитель является администратором чата
|
||||||
|
try:
|
||||||
|
if await check_moderator(message.from_user.id):
|
||||||
|
#Проверка отвечает ли сообщение на другое сообщение
|
||||||
|
if message.reply_to_message is not None:
|
||||||
|
time = message.text.split(' ')[1]
|
||||||
|
#получаем id отправителя сообщение на которое отвечает message
|
||||||
|
target_id = message.reply_to_message.from_user.id
|
||||||
|
target_name = cursor.execute("SELECT user_name FROM user_list WHERE user_id = ?", (target_id,)).fetchone()[0]
|
||||||
|
#Проверка, что человек пользователь
|
||||||
|
if cursor.execute("SELECT user_role FROM user_list WHERE user_id = ?", (target_id,)).fetchone()[0] == 0:
|
||||||
|
#ограничения прав пользователя по отправке сообщений на time секунд
|
||||||
|
time_sec = await time_to_seconds(message.text.split(' ')[1])
|
||||||
|
if time_sec <= 30 or time_sec >= 31536000:
|
||||||
|
await message.reply("Время мута должно быть больше 30 секунд и меньше 365 дней")
|
||||||
|
return
|
||||||
|
date_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
date_time = datetime.datetime.strptime(date_string, "%Y-%m-%d %H:%M:%S")
|
||||||
|
unix_time = int(mktime(date_time.timetuple()))
|
||||||
|
await bot.restrict_chat_member(message.chat.id, target_id, until_date=unix_time+time_sec, permissions=types.ChatPermissions(can_send_messages=False))
|
||||||
|
await message.reply(
|
||||||
|
f"Пользователь {target_name} замьючен на {await short_time_to_time(time)}")
|
||||||
|
else:
|
||||||
|
await message.reply(f"Пользователь [{target_name}](tg://user?id={target_id}) является {await get_role(target_id)} и не может быть замьючен",
|
||||||
|
parse_mode='Markdown')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
target_tag = message.text.split(' ')[1]
|
||||||
|
target_tag = target_tag[1:]
|
||||||
|
target_id = int(cursor.execute("SELECT user_id FROM user_list WHERE user_name = ?", (target_tag,)).fetchone()[0])
|
||||||
|
target_name = cursor.execute("SELECT user_name FROM user_list WHERE user_id = ?", (target_id,)).fetchone()[0]
|
||||||
|
#ограничения прав пользователя по отправке сообщений на time секунд
|
||||||
|
time_mute = message.text.split(' ')[2]
|
||||||
|
|
||||||
|
if cursor.execute("SELECT user_role FROM user_list WHERE user_id = ?", (target_id,)).fetchone()[0] == 0:
|
||||||
|
#ограничения прав пользователя по отправке сообщений на time секунд
|
||||||
|
time_sec = await time_to_seconds(message.text.split(' ')[2])
|
||||||
|
if time_sec <= 30 or time_sec >= 31536000:
|
||||||
|
await message.reply("Время мута должно быть больше 30 секунд и меньше 365 дней")
|
||||||
|
return
|
||||||
|
date_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
date_time = datetime.datetime.strptime(date_string, "%Y-%m-%d %H:%M:%S")
|
||||||
|
unix_time = int(mktime(date_time.timetuple()))
|
||||||
|
await bot.restrict_chat_member(message.chat.id, target_id, until_date=unix_time+time_sec, permissions=types.ChatPermissions(can_send_messages=False))
|
||||||
|
await message.reply(
|
||||||
|
f"Пользователь {target_name} замьючен на {await short_time_to_time(time_mute)}.")
|
||||||
|
else:
|
||||||
|
await message.reply(f"Пользователь [{target_name}](tg://user?id={target_id}) является {await get_role(target_id)} и не может быть замьючен",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
await message.reply("Ошибка данных. Возможно пользователь ещё ничего не написал.")
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message_handler(commands=['unmute'])
|
||||||
|
async def unmute(message: types.Message):
|
||||||
|
chat_id= message.chat.id
|
||||||
|
if await check_moderator(message.from_user.id):
|
||||||
|
if message.reply_to_message is not None:
|
||||||
|
target_id = message.reply_to_message.from_user.id
|
||||||
|
target_name = cursor.execute("SELECT user_name FROM user_list WHERE user_id = ?", (target_id,)).fetchone()[0]
|
||||||
|
await bot.restrict_chat_member(message.chat.id, target_id, until_date=0, permissions=types.ChatPermissions(can_send_messages=True))
|
||||||
|
await message.reply(
|
||||||
|
f"Пользователь [{target_name}](tg://user?id={target_id}) размьючен",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
else:
|
||||||
|
target_tag = message.text.split(' ')[1]
|
||||||
|
target_tag = target_tag[1:]
|
||||||
|
target_id = int(cursor.execute("SELECT user_id FROM user_list WHERE user_name = ?", (target_tag,)).fetchone()[0])
|
||||||
|
target_name = cursor.execute("SELECT user_name FROM user_list WHERE user_id = ?", (target_id,)).fetchone()[0]
|
||||||
|
#получаем стандартные права чата
|
||||||
|
|
||||||
|
if cursor.execute("SELECT user_role FROM user_list WHERE user_id = ?", (target_id,)).fetchone()[0] == 0:
|
||||||
|
#возвращаем пользователю стандартные права как у новых пользователей
|
||||||
|
chat_permisson = await bot.get_chat(chat_id)
|
||||||
|
await bot.restrict_chat_member(message.chat.id, target_id, until_date=0, permissions=chat_permisson.permissions)
|
||||||
|
await message.reply(
|
||||||
|
f"Пользователь [{target_name}](tg://user?id={target_id}) размьючен",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
|
||||||
|
|
||||||
|
#обрабатываем вход пользователя в чат
|
||||||
|
@dp.message_handler(content_types=['new_chat_members'])
|
||||||
|
async def new_chat_members(message: types.Message):
|
||||||
|
#проверяем что пользователь не бот
|
||||||
|
if not message.new_chat_members[0].is_bot:
|
||||||
|
#Заносим пользователя в базу данных
|
||||||
|
cursor.execute("INSERT INTO user_list VALUES (?, ?, ?, ?)", (message.new_chat_members[0].id, message.new_chat_members[0].username, 0, 0))
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message_handler(commands=['chatinfo'])
|
||||||
|
async def chat_info(message: types.Message):
|
||||||
|
#Выводит информацию о чате. Название, количество пользователей, количество администраторов, количество модераторов, количество сообщений.
|
||||||
|
if await check_moderator(message.from_user.id):
|
||||||
|
chat_id = message.chat.id
|
||||||
|
chat_title = message.chat.title
|
||||||
|
chat_members = await bot.get_chat_members_count(chat_id)
|
||||||
|
chat_admins = await bot.get_chat_administrators(chat_id)
|
||||||
|
chat_moderators = cursor.execute("SELECT COUNT(user_id) FROM user_list WHERE user_role = 1").fetchone()[0]
|
||||||
|
chat_messages = cursor.execute("SELECT chat_stats FROM chat_list WHERE chat_id = ?", (chat_id,)).fetchone()[0]
|
||||||
|
await message.reply(
|
||||||
|
f"<b>Название чата:</b> {chat_title}\n"
|
||||||
|
f"<b>Количество пользователей:</b> {chat_members}\n"
|
||||||
|
f"<b>Количество администраторов:</b> {len(chat_admins)}\n"
|
||||||
|
f"<b>Количество модераторов:</b> {chat_moderators}\n"
|
||||||
|
f"<b>Количество сообщений:</b> {chat_messages}",
|
||||||
|
parse_mode="HTML")
|
||||||
|
await top10(message)
|
||||||
|
else:
|
||||||
|
await message.reply(
|
||||||
|
f"У вас недостаточно прав для выполнения этой команды.")
|
||||||
|
|
||||||
|
@dp.message_handler(commands=['start'])
|
||||||
|
async def start(message: types.Message):
|
||||||
|
#проверка что чат есть в базе данных
|
||||||
|
if cursor.execute("SELECT chat_id FROM chat_list WHERE chat_id = ?", (message.chat.id,)).fetchone() is None:
|
||||||
|
chat_id = message.chat.id
|
||||||
|
chat_role = 0
|
||||||
|
chat_stats = 1
|
||||||
|
cursor.execute("INSERT INTO chat_list VALUES (?, ?, ?)", (chat_id, chat_role, chat_stats))
|
||||||
|
database.commit()
|
||||||
|
await message.reply(
|
||||||
|
f"{config['Telegram']['start_answer']}",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
else:
|
||||||
|
await message.reply(
|
||||||
|
f"Чат уже инициализирован.",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
# Проверка наличия столбца имени пользователя в базе данных, если его нет, то добавить.
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT user_name FROM user_list")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
cursor.execute("ALTER TABLE user_list ADD COLUMN user_name TEXT")
|
||||||
|
database.commit()
|
||||||
|
await message.reply("База данных пользователей реструктурирована.")
|
||||||
|
# Проверка наличия столбца репутации пользователя в базе данных, если его нет, то добавить.
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT user_rep FROM user_list")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
cursor.execute("ALTER TABLE user_list ADD COLUMN user_rep INTEGER")
|
||||||
|
database.commit()
|
||||||
|
await message.reply("База данных пользователей реструктурирована.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message_handler(commands=['top10'])
|
||||||
|
async def top10(message: types.Message):
|
||||||
|
#топ 10 пользователей по количеству сообщений в user_stats в формате: Имя пользователя - количество сообщений
|
||||||
|
top10 = cursor.execute("SELECT user_id, user_stats FROM user_list ORDER BY user_stats DESC LIMIT 10").fetchall()
|
||||||
|
top10_message = ''
|
||||||
|
for user in top10:
|
||||||
|
username = (cursor.execute("SELECT user_name FROM user_list WHERE user_id = ?", (user[0],)).fetchone())[0]
|
||||||
|
if username is None:
|
||||||
|
username = "Аноним"
|
||||||
|
top10_message += f"{username} - {user[1]}\n"
|
||||||
|
#в начале сообщения берём текст из config.ini и вставляем в него топ 10 пользователей
|
||||||
|
await message.reply(
|
||||||
|
f"{config['Telegram']['top10_answer']}\n{top10_message}")
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message_handler(commands=['aboutme'])
|
||||||
|
async def aboutme(message: types.Message):
|
||||||
|
your_id = message.from_id
|
||||||
|
await check(your_id)
|
||||||
|
your_name = message.from_user.username
|
||||||
|
user_stats = cursor.execute("SELECT user_stats FROM user_list WHERE user_id = ?", (message.from_user.id,)).fetchone()
|
||||||
|
rolenum = cursor.execute("SELECT user_role FROM user_list WHERE user_id = ?", (message.from_user.id,)).fetchone()
|
||||||
|
user_rep = cursor.execute("SELECT user_rep FROM user_list WHERE user_id = ?", (message.from_user.id,)).fetchone()
|
||||||
|
if "None" in str(user_rep):
|
||||||
|
user_rep = 0
|
||||||
|
cursor.execute("UPDATE user_list SET user_rep = ? WHERE user_id = ?", (user_rep, your_id))
|
||||||
|
role = await get_role_name(rolenum[0])
|
||||||
|
#Имя пользователя на следующей строке статистика сообщений на следующей строке права пользователя
|
||||||
|
#если пользователь есть в списке администраторов чата, то выдаём роль администратор
|
||||||
|
chat_admins = await bot.get_chat_administrators(message.chat.id)
|
||||||
|
default_for_admin = int(config['Roles']['default_for_admin'])
|
||||||
|
no_answer = False
|
||||||
|
if rolenum[0] < default_for_admin:
|
||||||
|
for admin in chat_admins:
|
||||||
|
if admin.user.id == your_id:
|
||||||
|
if default_for_admin == 0:
|
||||||
|
await message.reply(
|
||||||
|
f"<b>Вы обнаружены в списке администраторов чата!</b> \n"
|
||||||
|
f"<b>Вам будет выдана роль:</b> {config['Roles']['user']}\n"
|
||||||
|
f"<b>Имя:</b> <a href=\"tg://user?id={str(your_id)}\">{your_name}</a>\n"
|
||||||
|
f"<b>Кол-во сообщений:</b> {user_stats[0]}\n"
|
||||||
|
f"<b>Репутация:</b> {user_rep[0]}\n"
|
||||||
|
f"<b>Роль:</b> {role}",
|
||||||
|
parse_mode="HTML")
|
||||||
|
no_answer = True
|
||||||
|
elif default_for_admin == 1:
|
||||||
|
await message.reply(
|
||||||
|
f"<b>Вы обнаружены в списке администраторов чата!</b> \n"
|
||||||
|
f"<b>Вам будет выдана роль:</b> {config['Roles']['moderator']}\n"
|
||||||
|
f"<b>Имя:</b> <a href=\"tg://user?id={str(your_id)}\">{your_name}</a>\n"
|
||||||
|
f"<b>Кол-во сообщений:</b> {user_stats[0]}\n"
|
||||||
|
f"<b>Репутация:</b> {user_rep[0]}\n"
|
||||||
|
f"<b>Роль:</b> {role}",
|
||||||
|
parse_mode="HTML")
|
||||||
|
no_answer = True
|
||||||
|
elif default_for_admin == 2:
|
||||||
|
await message.reply(
|
||||||
|
f"<b>Вы обнаружены в списке администраторов чата!</b> \n"
|
||||||
|
f"<b>Вам будет выдана роль:</b> {config['Roles']['admin']}\n"
|
||||||
|
f"<b>Имя:</b> <a href=\"tg://user?id={str(your_id)}\">{your_name}</a>\n"
|
||||||
|
f"<b>Кол-во сообщений:</b> {user_stats[0]}\n"
|
||||||
|
f"<b>Репутация:</b> {user_rep[0]}\n"
|
||||||
|
f"<b>Роль:</b> {role}",
|
||||||
|
parse_mode="HTML")
|
||||||
|
no_answer = True
|
||||||
|
cursor.execute("UPDATE user_list SET user_role = ? WHERE user_id = ?", (default_for_admin, your_id))
|
||||||
|
database.commit()
|
||||||
|
if no_answer == False:
|
||||||
|
await message.reply(
|
||||||
|
f"<b>Имя:</b> <a href=\"tg://user?id={str(your_id)}\">{your_name}</a>\n"
|
||||||
|
f"<b>Кол-во сообщений:</b> {user_stats[0]}\n"
|
||||||
|
f"<b>Репутация:</b> {user_rep[0]}\n"
|
||||||
|
f"<b>Роль:</b> {role}",
|
||||||
|
parse_mode="HTML")
|
||||||
|
|
||||||
|
@dp.message_handler(commands=['setrole'])
|
||||||
|
async def setrole(message: types.Message):
|
||||||
|
try:
|
||||||
|
user_role = cursor.execute("SELECT user_role FROM user_list WHERE user_id = ?", (message.from_user.id,)).fetchone()
|
||||||
|
user_name = message.from_user.username
|
||||||
|
target_name = message.text.split()[1]
|
||||||
|
target_name = target_name[1:]
|
||||||
|
target_role = cursor.execute("SELECT user_role FROM user_list WHERE user_name = ?", (target_name,)).fetchone()[0]
|
||||||
|
user_id = cursor.execute("SELECT user_id FROM user_list WHERE user_name = ?", (target_name,)).fetchone()
|
||||||
|
user_id = user_id[0]
|
||||||
|
except:
|
||||||
|
await message.reply("Пользователь не найден!")
|
||||||
|
return
|
||||||
|
if await check_admin(user_id, message.chat.id) and target_role == 2:
|
||||||
|
await message.reply("Вы не можете изменить роль этому пользователю!")
|
||||||
|
else:
|
||||||
|
if user_role[0] == 2:
|
||||||
|
try:
|
||||||
|
#Получаем id пользователя по нику в базе данных
|
||||||
|
role = message.text.split()[2]
|
||||||
|
if role == "0" or role == config['Roles']['user']:
|
||||||
|
role = config['Roles']['user']
|
||||||
|
rolenum = 0
|
||||||
|
elif role == "1" or role == config['Roles']['moderator']:
|
||||||
|
role = config['Roles']['moderator']
|
||||||
|
rolenum = 1
|
||||||
|
elif role == "2" or role == config['Roles']['admin']:
|
||||||
|
role = config['Roles']['admin']
|
||||||
|
rolenum = 2
|
||||||
|
cursor.execute("UPDATE user_list SET user_role = ? WHERE user_id = ?", (rolenum, user_id))
|
||||||
|
database.commit()
|
||||||
|
await message.reply(
|
||||||
|
f"Пользователю [{target_name}](tg://user?id={str(user_id)}) выдана роль: {role}",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
except:
|
||||||
|
await message.reply(
|
||||||
|
f"Ошибка! Проверьте правильность написания команды.",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
else:
|
||||||
|
await message.reply(
|
||||||
|
f"Ошибка! У вас нет прав на использование этой команды.",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message_handler(commands=['about'])
|
||||||
|
async def about(message: types.Message):
|
||||||
|
# проверка что в сообщении есть тег пользователя
|
||||||
|
if len(message.text.split()) == 2:
|
||||||
|
try:
|
||||||
|
target_name = message.text.split()[1]
|
||||||
|
target_name = target_name[1:]
|
||||||
|
target_id = cursor.execute("SELECT user_id FROM user_list WHERE user_name = ?", (target_name,)).fetchone()
|
||||||
|
target_id = target_id[0]
|
||||||
|
user_rep = cursor.execute("SELECT user_rep FROM user_list WHERE user_id = ?", (target_id,)).fetchone()[0]
|
||||||
|
if "None" in str(user_rep):
|
||||||
|
user_rep = 0
|
||||||
|
cursor.execute("UPDATE user_list SET user_rep = ? WHERE user_id = ?", (user_rep, target_id))
|
||||||
|
await check(target_id)
|
||||||
|
except:
|
||||||
|
await message.reply(
|
||||||
|
f"Ошибка! Проверьте правильность написания тега пользователя.",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
user_stats = cursor.execute("SELECT user_stats FROM user_list WHERE user_id = ?", (target_id,)).fetchone()[0]
|
||||||
|
user_role = cursor.execute("SELECT user_role FROM user_list WHERE user_id = ?", (target_id,)).fetchone()[0]
|
||||||
|
user_role = await get_role_name(user_role)
|
||||||
|
await message.reply(
|
||||||
|
f"<b>Имя:</b> {target_name}\n"
|
||||||
|
f"<b>Кол-во сообщений:</b> {user_stats}\n"
|
||||||
|
f"<b>Репутация:</b> {user_rep}\n"
|
||||||
|
f"<b>Роль:</b> {user_role}", parse_mode="HTML")
|
||||||
|
else:
|
||||||
|
await message.reply(
|
||||||
|
f"Ошибка! Проверьте правильность написания команды.")
|
||||||
|
|
||||||
|
from src.OpenAI.GPT35turbo.OA_processing import openai_message_history_processing
|
||||||
|
|
||||||
|
@dp.message_handler(commands=['history'])
|
||||||
|
async def history(message: types.Message):
|
||||||
|
if message.reply_to_message is not None:
|
||||||
|
# Получаем id сообщения на которое отвечает message
|
||||||
|
start_message_id = message.reply_to_message.message_id
|
||||||
|
# Получаем id сообщения message
|
||||||
|
end_message_id = message.message_id
|
||||||
|
elif message.text.split()[1] != '':
|
||||||
|
end_message_id = message.message_id
|
||||||
|
start_message_id = end_message_id - int(message.text.split()[1])
|
||||||
|
else:
|
||||||
|
await message.reply("Ошибка! Проверьте правильность написания команды.")
|
||||||
|
return
|
||||||
|
temp_message = await message.reply("Подождите немного, я обрабатываю историю сообщений... Функция ЭКСПЕРЕМЕНТАЛЬНАЯ "
|
||||||
|
"и может работать некорректно.")
|
||||||
|
# Получаем историю сообщений
|
||||||
|
response = openai_message_history_processing(start_message_id, end_message_id)
|
||||||
|
if response is None:
|
||||||
|
await message.reply("Я не понял тебя, попробуй перефразировать")
|
||||||
|
else:
|
||||||
|
bot_message_id = await message.reply(response['choices'][0]['message']['content'], parse_mode="markdown")
|
||||||
|
# заносим сообщение в базу данных в качестве message_id мы пишем id сообщения в bot_message_id
|
||||||
|
cursor.execute("INSERT INTO message_list VALUES (?, ?, ?, ?)",
|
||||||
|
(bot_message_id.message_id, response['choices'][0]['message']['content'], 0, message.message_id))
|
||||||
|
# очищаем сообщение
|
||||||
|
asyncio.create_task(delete_message(temp_message, 0))
|
||||||
|
|
||||||
|
from src.OpenAI.GPT35turbo.OA_processing import openai_message_processing
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_message(message: types.Message, sleep_time: int = 0):
|
||||||
|
await asyncio.sleep(sleep_time)
|
||||||
|
with suppress(MessageCantBeDeleted, MessageToDeleteNotFound):
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
@dp.message_handler()
|
||||||
|
async def in_message(message: types.Message):
|
||||||
|
chat_id = message.chat.id
|
||||||
|
# Получение сообщений в чате, и запись их в базу данных
|
||||||
|
if (message.chat.type == "private" or message.chat.type == "channel"):
|
||||||
|
await message.reply(
|
||||||
|
f"{config['Telegram']['private_answer']}",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
elif (message.chat.type != "group" or message.chat.type != "supergroup") and \
|
||||||
|
message.text != '' and message.text != ' ' and \
|
||||||
|
(cursor.execute("SELECT chat_role FROM chat_list WHERE chat_id;") == 1): return None
|
||||||
|
else:
|
||||||
|
# Запись сообщения в базу данных
|
||||||
|
await save_message(message)
|
||||||
|
# Обработка сообщения OpenAI
|
||||||
|
send_answer = False
|
||||||
|
typing_mode = False
|
||||||
|
# импортируем массив триггеров из файла .ini
|
||||||
|
if message.reply_to_message and message.reply_to_message.from_user.id == (await bot.me).id:
|
||||||
|
send_answer = True
|
||||||
|
typing_mode = True
|
||||||
|
for trigger in bot_trigger_all:
|
||||||
|
if trigger.lower() in message.text.lower():
|
||||||
|
send_answer = True
|
||||||
|
typing_mode = False
|
||||||
|
for trigger in bot_trigger_front:
|
||||||
|
if message.text.lower().startswith(trigger.lower()):
|
||||||
|
send_answer = True
|
||||||
|
typing_mode = False
|
||||||
|
|
||||||
|
if send_answer == False:
|
||||||
|
# Если сообщение отвечает на другое сообщение, то проверяем наличие слова спасибо
|
||||||
|
if message.reply_to_message is not None:
|
||||||
|
if message.text.startswith("Спасибо"):
|
||||||
|
target_id = message.reply_to_message.from_user.id
|
||||||
|
target_name = cursor.execute("SELECT user_name FROM user_list WHERE user_id = ?", (target_id,)).fetchone()[0]
|
||||||
|
cursor.execute("UPDATE user_list SET user_rep = user_rep + 1 WHERE user_id = ?", (target_id,))
|
||||||
|
database.commit()
|
||||||
|
await message.reply(
|
||||||
|
f"Спасибо, [{target_name}](tg://user?id={str(target_id)}) за ваш ответ!",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
|
||||||
|
if send_answer:
|
||||||
|
if typing_mode is False:
|
||||||
|
your_id = message.from_id
|
||||||
|
your_name = message.from_user.username
|
||||||
|
temp_msg = await message.reply(
|
||||||
|
f"[{your_name}](tg://user?id={str(your_id)}), Подожди немного и я обязательно отвечу тебе!",
|
||||||
|
parse_mode="Markdown")
|
||||||
|
# Пишем что бот печатает
|
||||||
|
await bot.send_chat_action(message.chat.id, "typing")
|
||||||
|
# Получаем ответ от OpenAI
|
||||||
|
response = openai_message_processing(message.message_id)
|
||||||
|
if response is None:
|
||||||
|
bot_message_id = await message.reply("Я не понял тебя, попробуй перефразировать")
|
||||||
|
if typing_mode is False:
|
||||||
|
asyncio.create_task(delete_message(temp_msg, 0))
|
||||||
|
# заносим сообщение в базу данных в качестве message_id пишем id сообщения которое отправил бот
|
||||||
|
cursor.execute("INSERT INTO message_list VALUES (?, ?, ?, ?)",
|
||||||
|
(bot_message_id, "Я не понял тебя, попробуй перефразировать", 0, message.message_id))
|
||||||
|
else:
|
||||||
|
bot_message_id = await message.reply(response['choices'][0]['message']['content'], parse_mode="markdown")
|
||||||
|
if typing_mode is False:
|
||||||
|
asyncio.create_task(delete_message(temp_msg, 0))
|
||||||
|
# заносим сообщение в базу данных в качестве message_id мы пишем id сообщения в bot_message_id
|
||||||
|
cursor.execute("INSERT INTO message_list VALUES (?, ?, ?, ?)",
|
||||||
|
(bot_message_id.message_id, response['choices'][0]['message']['content'], 0, message.message_id))
|
||||||
|
# очищаем базу данных от старых сообщений
|
||||||
|
cursor.execute("DELETE FROM message_list WHERE message_id < ?", (bot_message_id.message_id - DB_message_limit,))
|
||||||
|
database.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
executor.start_polling(dp, skip_updates=True)
|
||||||
43
src/config.ini
Normal file
43
src/config.ini
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
[Telegram]
|
||||||
|
token=****
|
||||||
|
admin_password="Test_pass"
|
||||||
|
# Пока не используется
|
||||||
|
|
||||||
|
# Массивы заполнять через запятую. Пример: test|test2 |test3,
|
||||||
|
bot_trigger_front=
|
||||||
|
# Живой пример: Арма |Армат |Арма, |
|
||||||
|
bot_trigger_all=
|
||||||
|
# Живой пример: @arma_ai_bot |помогите |
|
||||||
|
private_answer=
|
||||||
|
# Живой пример: Я не понимаю тебя, но я могу поговорить с тобой в группе [название группы](https://t.me/) и ещё в некоторых других группах
|
||||||
|
reply_ignore=0
|
||||||
|
# По умолчанию: 0
|
||||||
|
# Содержит в себе все id топиков чата для чатов с форумным типом, если не заполнить контекст бота СЛОМАЕТСЯ!
|
||||||
|
# Пример заполнения для одного из чатов: 0| 643885| 476959| 1| 476977| 633077| 630664| 476966| 634567
|
||||||
|
start_answer= Привет! Я OCAB, открытый чат бот с ИИ для вашего чата!
|
||||||
|
top10_answer= Вот топ 10 самых разговорчивых пользователей чата:
|
||||||
|
|
||||||
|
[Roles]
|
||||||
|
user= Пользователь
|
||||||
|
moderator= Модератор
|
||||||
|
admin= Администратор
|
||||||
|
default_for_admin= 2
|
||||||
|
|
||||||
|
|
||||||
|
[Openai]
|
||||||
|
api_key=****
|
||||||
|
chat_model=gpt-3.5-turbo
|
||||||
|
story_model=
|
||||||
|
# Тут должен быть текст истории бота, но я его не показываю)))
|
||||||
|
max_token_count=4000
|
||||||
|
# максимальное количество токенов в сообщении
|
||||||
|
min_token_for_answer=800
|
||||||
|
# минимальное количество токенов в сообщении ответа бота (чем больше, тем более длинный ответ ГАРАНТИРОВАН)
|
||||||
|
|
||||||
|
[DataBase]
|
||||||
|
message_limit=3000
|
||||||
|
# Максимальное количество сообщений в базе данных
|
||||||
|
|
||||||
|
[AI_Dungeon]
|
||||||
|
use=openai
|
||||||
|
# "openai" or "YaGPT" Пока не используется
|
||||||
23
src/config.yaml
Normal file
23
src/config.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
TELEGRAM:
|
||||||
|
TOKEN: 1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
|
||||||
|
PRIVATE_ANSWER:
|
||||||
|
|
||||||
|
BOT:
|
||||||
|
ANSWER:
|
||||||
|
HELP_ANSWER:
|
||||||
|
START_ANSWER:
|
||||||
|
ABOUT_ANSWER:
|
||||||
|
|
||||||
|
ROLES:
|
||||||
|
ADMIN:
|
||||||
|
MODERATOR:
|
||||||
|
USER:
|
||||||
|
DEFAULT_FOR_ADMIN:
|
||||||
|
|
||||||
|
DATABASE:
|
||||||
|
MESSAGE_LIMIT:
|
||||||
|
|
||||||
|
GPT_MODULE:
|
||||||
|
TRIGGER_FRONT:
|
||||||
|
TRIGGER_ALL:
|
||||||
|
REPLY_IGNORE:
|
||||||
1
src/core.py
Normal file
1
src/core.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
FROM python:3.12-slim as builder
|
|
||||||
|
|
||||||
RUN pip install poetry
|
|
||||||
RUN mkdir -p /app
|
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
# Фикс
|
|
||||||
|
|
||||||
RUN sed -i '/ocab-core = {/{s/, develop = true//}' /app/src/gnomik/pyproject.toml && \
|
|
||||||
sed -i '/ocab-modules = {/{s/, develop = true//}' /app/src/gnomik/pyproject.toml && \
|
|
||||||
sed -i '/ocab-core = {/{s/, develop = true//}' /app/src/ocab_modules/pyproject.toml
|
|
||||||
|
|
||||||
WORKDIR /app/src/gnomik
|
|
||||||
|
|
||||||
RUN poetry lock && poetry install
|
|
||||||
|
|
||||||
FROM python:3.12-slim as base
|
|
||||||
|
|
||||||
COPY --from=builder /app/src/gnomik /app
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
ENV PATH="/app/.venv/bin:$PATH"
|
|
||||||
CMD ["python", "-m", "gnomik"]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
**/Dockerfile
|
|
||||||
**/*.dockerignore
|
|
||||||
**/docker-compose.yml
|
|
||||||
|
|
||||||
**/.git
|
|
||||||
**/.gitignore
|
|
||||||
|
|
||||||
**/.venv
|
|
||||||
|
|
||||||
**/.mypy_cache
|
|
||||||
**/__pycache__/
|
|
||||||
|
|
||||||
src/gnomik/config.yaml
|
|
||||||
src/gnomik/database/*
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Gnomик
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Чат-бот помощник в [ALT Gnome Chat](https://t.me/alt_gnome_chat).
|
|
||||||
|
|
||||||
|
|
||||||
ALT Regular Gnome Community - открытое сообщество пользователей операционной системы ALT Regular Gnome.
|
|
||||||
|
|
||||||
- [Канал](https://t.me/alt_gnome)
|
|
||||||
- [Wiki](https://alt-gnome.wiki)
|
|
||||||
|
|
||||||
## Описание
|
|
||||||
|
|
||||||
Gnomик - это чат-бот, разработанный на платформе Open Chat AI Bot (OCAB) для Telegram. Он предоставляет различные функции и возможности, помогающие пользователям операционной системы ALT Regular Gnome.
|
|
||||||
|
|
||||||
## Функционал
|
|
||||||
|
|
||||||
<!--
|
|
||||||
TODO: описать функционал
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Запуск
|
|
||||||
|
|
||||||
Запуск бота осуществляется с помощью команды:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m gnomik
|
|
||||||
```
|
|
||||||
|
|
||||||
## Конфигурация
|
|
||||||
|
|
||||||
Конфигурация бота находится в файле `config.yaml`.
|
|
||||||
|
|
||||||
## Модули
|
|
||||||
|
|
||||||
Список загружаемых модулей указан в файле `__main__.py`.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: src/gnomik/Dockerfile
|
|
||||||
ports:
|
|
||||||
- 9000:9000
|
|
||||||
volumes:
|
|
||||||
- ./config.yaml:/app/config.yaml
|
|
||||||
- ./database:/app/database
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
@@ -1,29 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
|
|
||||||
from ocab_core import OCAB
|
|
||||||
from ocab_modules import module_loader
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
ocab = OCAB()
|
|
||||||
await ocab.init_app(
|
|
||||||
[
|
|
||||||
module_loader("standard", "config", safe=False),
|
|
||||||
module_loader("standard", "database", safe=False),
|
|
||||||
module_loader("standard", "fsm_database_storage", safe=False),
|
|
||||||
module_loader("standard", "roles", safe=False),
|
|
||||||
module_loader("external", "yandexgpt", safe=False),
|
|
||||||
#
|
|
||||||
module_loader("standard", "command_helper"),
|
|
||||||
module_loader("standard", "info"),
|
|
||||||
module_loader("standard", "filters"),
|
|
||||||
module_loader("external", "create_report_apps"),
|
|
||||||
module_loader("standard", "admin"),
|
|
||||||
module_loader("standard", "message_processing"),
|
|
||||||
module_loader("standard", "miniapp", safe=False),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
await ocab.start()
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
2162
src/gnomik/poetry.lock
generated
2162
src/gnomik/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
|||||||
[virtualenvs]
|
|
||||||
in-project = true
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[tool.poetry]
|
|
||||||
name = "gnomik"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = ""
|
|
||||||
authors = ["Максим Слипенко <maxim@slipenko.com>"]
|
|
||||||
readme = "README.md"
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "~3.12"
|
|
||||||
ocab-core = { extras=["webhook"], path = "../ocab_core", develop = true }
|
|
||||||
ocab-modules = { path = "../ocab_modules", develop = true }
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
21
src/modules/Config.py
Normal file
21
src/modules/Config.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from yaml import load, Loader
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self):
|
||||||
|
self.bot_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.getcwd())))
|
||||||
|
self.config_dir = os.path.join(self.bot_dir, 'config.yaml')
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
return load(open(self.config_dir), Loader=Loader)
|
||||||
|
|
||||||
|
def get_bot_token(self):
|
||||||
|
return self.get_config()['BOT']['TOKEN']
|
||||||
|
|
||||||
|
def get_roles(self):
|
||||||
|
return self.get_config()['ROLES']
|
||||||
|
|
||||||
|
def get_bot_answers(self):
|
||||||
|
return self.get_config()['BOT']['ANSWER']
|
||||||
131
src/modules/DataBase.py
Normal file
131
src/modules/DataBase.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
class DataBase:
|
||||||
|
def __init__(self):
|
||||||
|
self.bot_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.getcwd())))
|
||||||
|
self.db = sqlite3.connect(os.path.join(self.bot_dir, 'DataBase/OCAB_DB.db'))
|
||||||
|
self.cursor = self.db.cursor()
|
||||||
|
|
||||||
|
# Проверки наличия таблиц в базе данных
|
||||||
|
def check_db(self):
|
||||||
|
self.check_chatdb()
|
||||||
|
self.check_userdb()
|
||||||
|
self.check_messagedb()
|
||||||
|
|
||||||
|
def check_chatdb(self):
|
||||||
|
self.cursor.execute("""SELECT name FROM sqlite_master WHERE type='table' AND name='chat_list'""")
|
||||||
|
if self.cursor.fetchone() is None:
|
||||||
|
self.create_chatdb()
|
||||||
|
|
||||||
|
def check_userdb(self):
|
||||||
|
self.cursor.execute("""SELECT name FROM sqlite_master WHERE type='table' AND name='user_list'""")
|
||||||
|
if self.cursor.fetchone() is None:
|
||||||
|
self.create_userdb()
|
||||||
|
|
||||||
|
def check_messagedb(self):
|
||||||
|
self.cursor.execute("""SELECT name FROM sqlite_master WHERE type='table' AND name='message_list'""")
|
||||||
|
if self.cursor.fetchone() is None:
|
||||||
|
self.create_messagedb()
|
||||||
|
|
||||||
|
# Создание таблиц в базе данных
|
||||||
|
def create_chatdb(self):
|
||||||
|
self.cursor.execute("""CREATE TABLE IF NOT EXISTS chat_list (
|
||||||
|
chat_id INTEGER PRIMARY KEY,
|
||||||
|
chat_role INTEGER NOT NULL,
|
||||||
|
chat_stats INTEGER NOT NULL,
|
||||||
|
chat_federation INTEGER,
|
||||||
|
)""")
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
def create_userdb(self):
|
||||||
|
self.cursor.execute("""CREATE TABLE IF NOT EXISTS user_list (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
user_name TEXT NOT NULL,
|
||||||
|
user_tag TEXT,
|
||||||
|
user_role INTEGER,
|
||||||
|
user_stats INTEGER
|
||||||
|
user_rep INTEGER
|
||||||
|
)""")
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
def create_messagedb(self):
|
||||||
|
self.cursor.execute("""CREATE TABLE IF NOT EXISTS message_list (
|
||||||
|
message_id INTEGER PRIMARY KEY,
|
||||||
|
message_text TEXT NOT NULL,
|
||||||
|
message_id_sender INTEGER NOT NULL,
|
||||||
|
answer_to_id INTEGER
|
||||||
|
)""")
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def add_chat(self, chat_id, chat_role, chat_stats=0, chat_federation=0):
|
||||||
|
self.cursor.execute("""INSERT INTO chat_list VALUES (?, ?, ?, ?)""",
|
||||||
|
(chat_id, chat_role, chat_stats, chat_federation))
|
||||||
|
|
||||||
|
async def add_user(self, user_id, user_name, user_tag=None, user_role=0, user_stats=0, user_rep=0):
|
||||||
|
self.cursor.execute("""INSERT INTO user_list VALUES (?, ?, ?, ?, ?, ?)""",
|
||||||
|
(user_id, user_name, user_tag, user_role, user_stats, user_rep))
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def add_message(self, message_id, message_text, message_sender, answer_id):
|
||||||
|
self.cursor.execute("""INSERT INTO message_list VALUES (?, ?, ?, ?)""",
|
||||||
|
(message_id, message_text, message_sender, answer_id))
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def get_chat(self, chat_id):
|
||||||
|
self.cursor.execute("""SELECT * FROM chat_list WHERE chat_id=?""", (chat_id,))
|
||||||
|
return self.cursor.fetchone()
|
||||||
|
|
||||||
|
async def get_user(self, user_id):
|
||||||
|
self.cursor.execute("""SELECT * FROM user_list WHERE user_id=?""", (user_id,))
|
||||||
|
return self.cursor.fetchone()
|
||||||
|
|
||||||
|
async def get_user_role(self, user_id):
|
||||||
|
self.cursor.execute("""SELECT user_role FROM user_list WHERE user_id=?""", (user_id,))
|
||||||
|
return self.cursor.fetchone()
|
||||||
|
|
||||||
|
async def get_message(self, message_id):
|
||||||
|
self.cursor.execute("""SELECT * FROM message_list WHERE message_id=?""", (message_id,))
|
||||||
|
return self.cursor.fetchone()
|
||||||
|
|
||||||
|
async def change_user_name(self, user_id, new_user_name):
|
||||||
|
self.cursor.execute("""UPDATE user_list SET user_name=? WHERE user_id=?""", (new_user_name, user_id))
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def change_user_role(self, user_id, new_user_role):
|
||||||
|
self.cursor.execute("""UPDATE user_list SET user_role=? WHERE user_id=?""", (new_user_role, user_id))
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def change_user_stats(self, user_id, new_user_stats):
|
||||||
|
self.cursor.execute("""UPDATE user_list SET user_stats=? WHERE user_id=?""", (new_user_stats, user_id))
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def change_user_rep(self, user_id, new_user_rep):
|
||||||
|
self.cursor.execute("""UPDATE user_list SET user_rep=? WHERE user_id=?""", (new_user_rep, user_id))
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def change_chat_role(self, chat_id, new_chat_role):
|
||||||
|
self.cursor.execute("""UPDATE chat_list SET chat_role=? WHERE chat_id=?""", (new_chat_role, chat_id))
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def change_chat_stats(self, chat_id, new_chat_stats):
|
||||||
|
self.cursor.execute("""UPDATE chat_list SET chat_stats=? WHERE chat_id=?""", (new_chat_stats, chat_id))
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def change_chat_federation(self, chat_id, new_chat_federation):
|
||||||
|
self.cursor.execute("""UPDATE chat_list SET chat_federation=? WHERE chat_id=?""",
|
||||||
|
(new_chat_federation, chat_id))
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def update_user_stats(self):
|
||||||
|
self.cursor.execute("""UPDATE user_list SET user_stats=user_stats+1""")
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def update_user_rep(self):
|
||||||
|
self.cursor.execute("""UPDATE user_list SET user_rep=user_rep+1""")
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
async def update_chat_stats(self):
|
||||||
|
self.cursor.execute("""UPDATE chat_list SET chat_stats=chat_stats+1""")
|
||||||
|
self.db.commit()
|
||||||
33
src/modules/Roles.py
Normal file
33
src/modules/Roles.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from Config import Config
|
||||||
|
from DataBase import DataBase
|
||||||
|
|
||||||
|
|
||||||
|
class Roles:
|
||||||
|
def __init__(self):
|
||||||
|
self.DB = DataBase()
|
||||||
|
self.Config = Config()
|
||||||
|
self.user_role_name = self.Config.get_roles()['USER']
|
||||||
|
self.moderator_role_name = self.Config.get_roles()['MODERATOR']
|
||||||
|
self.admin_role_name = self.Config.get_roles()['ADMIN']
|
||||||
|
|
||||||
|
async def check_admin_permission(self, user_id):
|
||||||
|
if await self.DB.get_user_role(user_id) == 2:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def check_moderator_permission(self, user_id):
|
||||||
|
if await self.DB.get_user_role(user_id) == 1:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_role_name(self, role_number):
|
||||||
|
if role_number == 0:
|
||||||
|
return self.user_role_name
|
||||||
|
elif role_number == 1:
|
||||||
|
return self.moderator_role_name
|
||||||
|
elif role_number == 2:
|
||||||
|
return self.admin_role_name
|
||||||
|
else:
|
||||||
|
return None
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# OCAB Core
|
|
||||||
|
|
||||||
Это ядро OCAB, содержащее базовые компоненты:
|
|
||||||
|
|
||||||
- Система управления модулями.
|
|
||||||
- Логирование.
|
|
||||||
- Утилиты.
|
|
||||||
|
|
||||||
## Система управления модулями
|
|
||||||
|
|
||||||
Система управления модулями отвечает за:
|
|
||||||
|
|
||||||
- Загрузку модулей.
|
|
||||||
- Проверку зависимостей.
|
|
||||||
- Предоставление API для взаимодействия между модулями.
|
|
||||||
|
|
||||||
## Логирование
|
|
||||||
|
|
||||||
Модуль логирования предоставляет функции для записи логов в консоль.
|
|
||||||
|
|
||||||
## Утилиты
|
|
||||||
|
|
||||||
Модуль утилит содержит вспомогательные функции, например, для форматирования текста.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .main import OCAB
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import importlib
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher
|
|
||||||
from aiogram.types import Update
|
|
||||||
|
|
||||||
|
|
||||||
def get_module_directory(module_name):
|
|
||||||
spec = importlib.util.find_spec(module_name)
|
|
||||||
if spec is None:
|
|
||||||
raise ImportError(f"Module {module_name} not found")
|
|
||||||
module_path = spec.origin
|
|
||||||
if module_path is None:
|
|
||||||
raise ImportError(f"Module {module_name} has no origin path")
|
|
||||||
return os.path.dirname(module_path)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
|
|
||||||
async def register_bot_webhook(app: FastAPI, bot: Bot, dp: Dispatcher):
|
|
||||||
async def handle_webhook(request: Request):
|
|
||||||
try:
|
|
||||||
update = Update.model_validate(
|
|
||||||
await request.json(), context={"bot": bot}
|
|
||||||
)
|
|
||||||
await dp.feed_update(bot, update)
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
return {"ok": False}
|
|
||||||
|
|
||||||
return {"ok": True}
|
|
||||||
|
|
||||||
app.post("/webhook")(handle_webhook)
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import logging
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
app_logger = logging.getLogger("ocab")
|
|
||||||
log_level = logging.INFO
|
|
||||||
|
|
||||||
|
|
||||||
def patch_logger(logger_: logging.Logger):
|
|
||||||
logger_.handlers = []
|
|
||||||
formatter = logging.Formatter("%(asctime)s %(message)s", datefmt="%H:%M:%S")
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_handler.setFormatter(formatter)
|
|
||||||
logger_.addHandler(console_handler)
|
|
||||||
logger_.propagate = False
|
|
||||||
logger_.setLevel(log_level)
|
|
||||||
return logger_
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logger():
|
|
||||||
"""
|
|
||||||
Настройка логирования
|
|
||||||
"""
|
|
||||||
patch_logger(app_logger)
|
|
||||||
|
|
||||||
|
|
||||||
def log(message):
|
|
||||||
if isinstance(message, Exception):
|
|
||||||
error_message = f"Error: {str(message)}\n{traceback.format_exc()}"
|
|
||||||
app_logger.error(error_message)
|
|
||||||
else:
|
|
||||||
app_logger.info(message)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from hypercorn.logging import Logger as HypercornLogger
|
|
||||||
|
|
||||||
class CustomLogger(HypercornLogger):
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
if self.error_logger:
|
|
||||||
patch_logger(self.error_logger)
|
|
||||||
if self.access_logger:
|
|
||||||
patch_logger(self.access_logger)
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import traceback
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher
|
|
||||||
|
|
||||||
from ocab_core.lib import register_bot_webhook
|
|
||||||
from ocab_core.logger import CustomLogger, log, setup_logger
|
|
||||||
from ocab_core.modules_system import ModulesManager
|
|
||||||
from ocab_core.modules_system.public_api import get_module
|
|
||||||
from ocab_core.singleton import Singleton
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ocab_modules.standard.config import IConfig
|
|
||||||
|
|
||||||
|
|
||||||
class OCAB:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def init_app(self, bot_modules):
|
|
||||||
setup_logger()
|
|
||||||
singleton = Singleton()
|
|
||||||
|
|
||||||
try:
|
|
||||||
singleton.modules_manager = ModulesManager()
|
|
||||||
|
|
||||||
for module_loader in bot_modules:
|
|
||||||
info = module_loader.info()
|
|
||||||
log(f"Loading {info.name} ({info.id}) module")
|
|
||||||
await singleton.modules_manager.load(module_loader)
|
|
||||||
|
|
||||||
register_config()
|
|
||||||
|
|
||||||
config: "IConfig" = get_module("standard.config", "config")
|
|
||||||
|
|
||||||
config.load()
|
|
||||||
|
|
||||||
singleton.bot = Bot(token=config.get("core::token"))
|
|
||||||
singleton.dp = Dispatcher(storage=singleton.storage["_fsm_storage"])
|
|
||||||
singleton.dp.include_routers(*singleton.storage["_routers"])
|
|
||||||
|
|
||||||
for middleware in singleton.storage["_outer_message_middlewares"]:
|
|
||||||
singleton.dp.message.outer_middleware.register(middleware)
|
|
||||||
|
|
||||||
await singleton.modules_manager.late_init()
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def start(self):
|
|
||||||
config: "IConfig" = get_module("standard.config", "config")
|
|
||||||
|
|
||||||
if config.get("core::mode") == "WEBHOOK":
|
|
||||||
await self.start_webhook_mode()
|
|
||||||
else:
|
|
||||||
await self.start_long_polling_mode()
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
async def start_long_polling_mode(self):
|
|
||||||
singleton = Singleton()
|
|
||||||
await singleton.bot.delete_webhook()
|
|
||||||
await singleton.dp.start_polling(singleton.bot)
|
|
||||||
|
|
||||||
async def start_webhook_mode(self):
|
|
||||||
try:
|
|
||||||
from fastapi import FastAPI
|
|
||||||
from hypercorn.asyncio import serve
|
|
||||||
from hypercorn.config import Config as HyperConfig
|
|
||||||
except ImportError:
|
|
||||||
log(
|
|
||||||
"Error: FastAPI and Hypercorn are required"
|
|
||||||
"for webhook mode. Please install them."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
singleton = Singleton()
|
|
||||||
app = FastAPI()
|
|
||||||
config = get_module("standard.config", "config")
|
|
||||||
app.mount("/webapp", singleton.storage["webapp"])
|
|
||||||
await register_bot_webhook(app, singleton.bot, singleton.dp)
|
|
||||||
await singleton.bot.set_webhook(config.get("core::webhook::public_url"))
|
|
||||||
hyperConfig = HyperConfig()
|
|
||||||
hyperConfig.bind = [f"0.0.0.0:{config.get("core::webhook::port")}"]
|
|
||||||
hyperConfig.logger_class = CustomLogger
|
|
||||||
await serve(app, hyperConfig)
|
|
||||||
|
|
||||||
|
|
||||||
def register_config():
|
|
||||||
config: "IConfig" = get_module("standard.config", "config")
|
|
||||||
|
|
||||||
config.register(
|
|
||||||
"core::token",
|
|
||||||
"password",
|
|
||||||
visible=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
config.register(
|
|
||||||
"core::mode",
|
|
||||||
"select",
|
|
||||||
options=["WEBHOOK", "LONG_POLLING"],
|
|
||||||
default_value="WEBHOOK",
|
|
||||||
visible=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
config.register(
|
|
||||||
"core::webhook::port",
|
|
||||||
"int",
|
|
||||||
default_value=9000,
|
|
||||||
visible=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
config.register(
|
|
||||||
"core::webhook::public_url",
|
|
||||||
"string",
|
|
||||||
visible=False,
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .modules_manager import ModulesManager
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from .fs_loader import FSLoader
|
|
||||||
from .unsafe_fs_loader import UnsafeFSLoader
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import types
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Dict, List, Optional, Union
|
|
||||||
|
|
||||||
from dataclasses_json import dataclass_json
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass_json
|
|
||||||
@dataclass
|
|
||||||
class DependencyInfo:
|
|
||||||
version: str
|
|
||||||
uses: Optional[List[str]] = None
|
|
||||||
|
|
||||||
|
|
||||||
DependencyType = Union[str, DependencyInfo]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass_json
|
|
||||||
@dataclass
|
|
||||||
class Dependencies:
|
|
||||||
required: Optional[Dict[str, DependencyType]] = None
|
|
||||||
optional: Optional[Dict[str, DependencyType]] = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass_json
|
|
||||||
@dataclass
|
|
||||||
class ModuleInfo:
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
version: str
|
|
||||||
author: Union[str, List[str]]
|
|
||||||
privileged: bool
|
|
||||||
dependencies: Dependencies
|
|
||||||
pythonDependencies: Optional[Dependencies] = None
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractLoader:
|
|
||||||
def info(self) -> ModuleInfo:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def load(self) -> types.ModuleType:
|
|
||||||
raise NotImplementedError
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import types
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from RestrictedPython import compile_restricted_exec
|
|
||||||
|
|
||||||
# from ocab_core.logger import log
|
|
||||||
from ocab_core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader
|
|
||||||
from ocab_core.modules_system.safe.policy import (
|
|
||||||
ALLOWED_IMPORTS,
|
|
||||||
BUILTINS,
|
|
||||||
RestrictedPythonPolicy,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FSLoader(UnsafeFSLoader):
|
|
||||||
def __init__(self, path):
|
|
||||||
super().__init__(path)
|
|
||||||
self.builtins = BUILTINS.copy()
|
|
||||||
self.builtins["__import__"] = self._hook_import
|
|
||||||
self.module_info = self.info()
|
|
||||||
self.allowed_python_dependencies = self._get_allowed_python_dependencies()
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
if self.module_info.privileged:
|
|
||||||
raise Exception("Only non privileged modules are allowed to be imported")
|
|
||||||
self.module_id = self.module_info.id
|
|
||||||
|
|
||||||
return self._hook_import(".")
|
|
||||||
|
|
||||||
def _get_allowed_python_dependencies(self):
|
|
||||||
allowed = {}
|
|
||||||
|
|
||||||
if self.module_info.pythonDependencies:
|
|
||||||
if self.module_info.pythonDependencies.required:
|
|
||||||
allowed.update(self.module_info.pythonDependencies.required)
|
|
||||||
if self.module_info.pythonDependencies.optional:
|
|
||||||
allowed.update(self.module_info.pythonDependencies.optional)
|
|
||||||
|
|
||||||
for allowed_module in ALLOWED_IMPORTS:
|
|
||||||
allowed[allowed_module] = "*"
|
|
||||||
|
|
||||||
return allowed
|
|
||||||
|
|
||||||
def _resolve_module_from_path(self, module_name: str):
|
|
||||||
path = Path(self.path)
|
|
||||||
|
|
||||||
if module_name != ".":
|
|
||||||
path = path.joinpath(module_name.replace(".", "/"))
|
|
||||||
|
|
||||||
if path.is_dir():
|
|
||||||
init_file_path = path / "__init__.py"
|
|
||||||
if not init_file_path.exists():
|
|
||||||
raise FileNotFoundError(f"File {init_file_path} does not exist.")
|
|
||||||
file_path = init_file_path
|
|
||||||
else:
|
|
||||||
path = path.with_suffix(".py")
|
|
||||||
if path.is_file():
|
|
||||||
file_path = path
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Module not found: {module_name}")
|
|
||||||
|
|
||||||
return file_path
|
|
||||||
|
|
||||||
def _hook_import(self, name: str, *args, **kwargs):
|
|
||||||
if name == "ocab_core.modules_system.public_api":
|
|
||||||
module = __import__(name, *args, **kwargs)
|
|
||||||
module.__ocab_module_id__ = self.module_id
|
|
||||||
return module
|
|
||||||
|
|
||||||
for key in self.allowed_python_dependencies.keys():
|
|
||||||
if name == key or name.startswith(f"{key}."):
|
|
||||||
return __import__(name, *args, **kwargs)
|
|
||||||
|
|
||||||
module_file_path = self._resolve_module_from_path(name)
|
|
||||||
|
|
||||||
with open(module_file_path, "r") as f:
|
|
||||||
src = f.read()
|
|
||||||
|
|
||||||
module = types.ModuleType(name)
|
|
||||||
module.__dict__.update(
|
|
||||||
{"__builtins__": self.builtins, "__ocab_module_id__": self.module_id}
|
|
||||||
)
|
|
||||||
result = compile_restricted_exec(src, "<string>", policy=RestrictedPythonPolicy)
|
|
||||||
|
|
||||||
if result.errors:
|
|
||||||
for error in result.errors:
|
|
||||||
print(error)
|
|
||||||
|
|
||||||
exec(result.code, module.__dict__) # nosec
|
|
||||||
|
|
||||||
return module
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .FSLoader import FSLoader
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import importlib.util
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from ocab_core.modules_system.loaders.base import AbstractLoader, ModuleInfo
|
|
||||||
|
|
||||||
|
|
||||||
class UnsafeFSLoader(AbstractLoader):
|
|
||||||
def __init__(self, path):
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
def info(self):
|
|
||||||
with open(os.path.join(self.path, "info.json"), "r") as f:
|
|
||||||
return ModuleInfo.from_json(f.read())
|
|
||||||
|
|
||||||
def _resolve_module_from_path(self, module_name: str):
|
|
||||||
path = Path(self.path)
|
|
||||||
|
|
||||||
if module_name != ".":
|
|
||||||
path = path.joinpath(module_name.replace(".", "/"))
|
|
||||||
|
|
||||||
if path.is_dir():
|
|
||||||
init_file_path = path / "__init__.py"
|
|
||||||
if not init_file_path.exists():
|
|
||||||
raise FileNotFoundError(f"File {init_file_path} does not exist.")
|
|
||||||
file_path = init_file_path
|
|
||||||
else:
|
|
||||||
path = path.with_suffix(".py")
|
|
||||||
if path.is_file():
|
|
||||||
file_path = path
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Module not found: {module_name}")
|
|
||||||
|
|
||||||
return file_path
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
self.info()
|
|
||||||
|
|
||||||
full_path = self._resolve_module_from_path(".")
|
|
||||||
|
|
||||||
if full_path.name == "__init__.py":
|
|
||||||
module_name = full_path.parent.name
|
|
||||||
path = full_path.parent.parent.absolute()
|
|
||||||
else:
|
|
||||||
module_name = full_path.stem
|
|
||||||
path = full_path.parent.absolute()
|
|
||||||
|
|
||||||
# Добавляем директорию модуля в sys.path
|
|
||||||
sys.path.insert(0, str(path))
|
|
||||||
|
|
||||||
# Загружаем спецификацию модуля
|
|
||||||
spec = importlib.util.spec_from_file_location(module_name, full_path)
|
|
||||||
|
|
||||||
# Создаем модуль
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
|
|
||||||
# Выполняем модуль
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
|
|
||||||
return module
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .UnsafeFSLoader import UnsafeFSLoader
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import importlib
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
import pkg_resources
|
|
||||||
import semver
|
|
||||||
|
|
||||||
from ocab_core.modules_system.loaders.base import (
|
|
||||||
AbstractLoader,
|
|
||||||
DependencyInfo,
|
|
||||||
ModuleInfo,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def is_version_compatible(version, requirement):
|
|
||||||
def parse_requirement(req):
|
|
||||||
if req.startswith("^"):
|
|
||||||
base_version = req[1:]
|
|
||||||
base_version_info = semver.VersionInfo.parse(base_version)
|
|
||||||
range_start = base_version_info
|
|
||||||
range_end = base_version_info.bump_major()
|
|
||||||
return [f">={range_start}", f"<{range_end}"]
|
|
||||||
else:
|
|
||||||
return [req]
|
|
||||||
|
|
||||||
for r in parse_requirement(requirement):
|
|
||||||
if r == "*":
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not semver.Version.parse(version).match(r):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def check_python_dependencies(info: ModuleInfo):
|
|
||||||
if info.pythonDependencies and info.pythonDependencies.required:
|
|
||||||
for dependency, req in info.pythonDependencies.required.items():
|
|
||||||
try:
|
|
||||||
importlib.import_module(dependency)
|
|
||||||
except ImportError:
|
|
||||||
raise Exception(
|
|
||||||
f"Module {info.id} requires {dependency}, "
|
|
||||||
f"but it is not installed"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
installed_version = pkg_resources.get_distribution(dependency).version
|
|
||||||
except pkg_resources.DistributionNotFound:
|
|
||||||
installed_version = "*"
|
|
||||||
|
|
||||||
if isinstance(req, str):
|
|
||||||
required_version = req
|
|
||||||
elif isinstance(req, DependencyInfo):
|
|
||||||
required_version = req.version
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Invalid dependency specification for {dependency}")
|
|
||||||
|
|
||||||
if not is_version_compatible(installed_version, required_version):
|
|
||||||
raise Exception(
|
|
||||||
f"Module {info.id} depends on {dependency} {required_version}, "
|
|
||||||
f"but version {installed_version} is installed"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def check_dependency_uses(
|
|
||||||
loaded_dependency, required_uses, dependent_module_id, dependency_id
|
|
||||||
):
|
|
||||||
module = loaded_dependency.get("module")
|
|
||||||
if not module:
|
|
||||||
raise Exception(f"Module object not found for dependency {dependency_id}")
|
|
||||||
|
|
||||||
for required_attr in required_uses:
|
|
||||||
if not hasattr(module, required_attr):
|
|
||||||
raise Exception(
|
|
||||||
f"Module {dependent_module_id} requires '{required_attr}' "
|
|
||||||
f"from {dependency_id}, but it is not available"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def await_if_async(module, method_name):
|
|
||||||
if hasattr(module, method_name):
|
|
||||||
method = getattr(module, method_name)
|
|
||||||
if inspect.iscoroutinefunction(method):
|
|
||||||
await method()
|
|
||||||
else:
|
|
||||||
method()
|
|
||||||
|
|
||||||
|
|
||||||
class ModulesManager:
|
|
||||||
def __init__(self):
|
|
||||||
self.modules = []
|
|
||||||
|
|
||||||
async def load(self, loader: AbstractLoader):
|
|
||||||
info = loader.info()
|
|
||||||
|
|
||||||
# Check if the module is already loaded
|
|
||||||
if any(mod["info"].id == info.id for mod in self.modules):
|
|
||||||
return
|
|
||||||
|
|
||||||
self.check_module_dependencies(info)
|
|
||||||
check_python_dependencies(info)
|
|
||||||
|
|
||||||
module_info = {
|
|
||||||
"info": info,
|
|
||||||
"module": None,
|
|
||||||
}
|
|
||||||
self.modules.append(module_info)
|
|
||||||
module = loader.load()
|
|
||||||
module_info["module"] = module
|
|
||||||
|
|
||||||
await await_if_async(module, "module_init")
|
|
||||||
|
|
||||||
def check_module_dependencies(self, info: ModuleInfo):
|
|
||||||
if info.dependencies.required:
|
|
||||||
for dependency, req in info.dependencies.required.items():
|
|
||||||
loaded_dependency = next(
|
|
||||||
(mod for mod in self.modules if mod["info"].id == dependency), None
|
|
||||||
)
|
|
||||||
if not loaded_dependency:
|
|
||||||
raise Exception(
|
|
||||||
f"Module {info.id} depends on {dependency},"
|
|
||||||
f"but it is not loaded"
|
|
||||||
)
|
|
||||||
|
|
||||||
loaded_dependency_info = loaded_dependency["info"]
|
|
||||||
|
|
||||||
if isinstance(req, str):
|
|
||||||
required_version = req
|
|
||||||
elif isinstance(req, DependencyInfo):
|
|
||||||
required_version = req.version
|
|
||||||
if req.uses:
|
|
||||||
check_dependency_uses(
|
|
||||||
loaded_dependency, req.uses, info.id, dependency
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid dependency specification for {dependency}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not is_version_compatible(
|
|
||||||
loaded_dependency_info.version, required_version
|
|
||||||
):
|
|
||||||
raise Exception(
|
|
||||||
f"Module {info.id} depends on {dependency} {required_version}, "
|
|
||||||
f"but version {loaded_dependency_info.version} is loaded"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def late_init(self):
|
|
||||||
for m in self.modules:
|
|
||||||
module = m["module"]
|
|
||||||
await await_if_async(module, "module_late_init")
|
|
||||||
|
|
||||||
def get_by_id(self, module_id: str):
|
|
||||||
module = next(
|
|
||||||
(mod for mod in self.modules if mod["info"].id == module_id), None
|
|
||||||
)
|
|
||||||
if not module:
|
|
||||||
raise Exception(f"Module with id {module_id} not loaded")
|
|
||||||
|
|
||||||
return module["module"]
|
|
||||||
|
|
||||||
def get_info_by_id(self, module_id: str):
|
|
||||||
module = next(
|
|
||||||
(mod for mod in self.modules if mod["info"].id == module_id), None
|
|
||||||
)
|
|
||||||
if not module:
|
|
||||||
raise Exception(f"Module with id {module_id} not loaded")
|
|
||||||
|
|
||||||
return module["info"]
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
from ocab_core.logger import log
|
|
||||||
|
|
||||||
from .public_api import (
|
|
||||||
Storage,
|
|
||||||
get_fsm_context,
|
|
||||||
get_module,
|
|
||||||
register_outer_message_middleware,
|
|
||||||
register_router,
|
|
||||||
set_chat_menu_button,
|
|
||||||
set_my_commands,
|
|
||||||
)
|
|
||||||
from .utils import Utils
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import inspect
|
|
||||||
import types
|
|
||||||
from typing import Any, Tuple, Union
|
|
||||||
|
|
||||||
from aiogram import BaseMiddleware, Router
|
|
||||||
from aiogram.fsm.context import FSMContext
|
|
||||||
from aiogram.fsm.storage.base import StorageKey
|
|
||||||
|
|
||||||
# from ocab_core.logger import log
|
|
||||||
from ocab_core.modules_system.loaders.base import DependencyInfo
|
|
||||||
from ocab_core.singleton import Singleton
|
|
||||||
|
|
||||||
|
|
||||||
async def set_chat_menu_button(menu_button):
|
|
||||||
app = Singleton()
|
|
||||||
await app.bot.set_chat_menu_button(menu_button=menu_button)
|
|
||||||
|
|
||||||
|
|
||||||
def register_router(router: Router):
|
|
||||||
app = Singleton()
|
|
||||||
app.storage["_routers"].append(router)
|
|
||||||
|
|
||||||
|
|
||||||
def register_outer_message_middleware(middleware: BaseMiddleware):
|
|
||||||
app = Singleton()
|
|
||||||
app.storage["_outer_message_middlewares"].append(middleware)
|
|
||||||
|
|
||||||
|
|
||||||
async def set_my_commands(commands):
|
|
||||||
app = Singleton()
|
|
||||||
await app.bot.set_my_commands(commands)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_fsm_context(chat_id: int, user_id: int) -> FSMContext:
|
|
||||||
dp = Singleton().dp
|
|
||||||
bot = Singleton().bot
|
|
||||||
|
|
||||||
return FSMContext(
|
|
||||||
storage=dp.storage,
|
|
||||||
key=StorageKey(
|
|
||||||
chat_id=chat_id,
|
|
||||||
user_id=user_id,
|
|
||||||
bot_id=bot.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def set_fsm(storage):
|
|
||||||
app = Singleton()
|
|
||||||
app.storage["_fsm_storage"] = storage
|
|
||||||
|
|
||||||
|
|
||||||
def get_module(
|
|
||||||
module_id: str, paths=None
|
|
||||||
) -> Union[types.ModuleType, Union[Any, None], Tuple[Union[Any, None], ...]]:
|
|
||||||
|
|
||||||
caller_globals = inspect.currentframe().f_back.f_globals
|
|
||||||
app = Singleton()
|
|
||||||
|
|
||||||
allowed_uses = None
|
|
||||||
|
|
||||||
if "__ocab_module_id__" in caller_globals:
|
|
||||||
caller_module_id = caller_globals["__ocab_module_id__"]
|
|
||||||
caller_module_info = app.modules_manager.get_info_by_id(caller_module_id)
|
|
||||||
|
|
||||||
if caller_module_info and caller_module_info.dependencies:
|
|
||||||
dependency = None
|
|
||||||
if caller_module_info.dependencies.required:
|
|
||||||
dependency = caller_module_info.dependencies.required.get(module_id)
|
|
||||||
if not dependency and caller_module_info.dependencies.optional:
|
|
||||||
dependency = caller_module_info.dependencies.optional.get(module_id)
|
|
||||||
|
|
||||||
if (
|
|
||||||
dependency
|
|
||||||
and isinstance(dependency, DependencyInfo)
|
|
||||||
and dependency.uses
|
|
||||||
):
|
|
||||||
allowed_uses = set(dependency.uses)
|
|
||||||
|
|
||||||
module = app.modules_manager.get_by_id(module_id)
|
|
||||||
|
|
||||||
if not module:
|
|
||||||
raise ModuleNotFoundError(f"Module {module_id} not found")
|
|
||||||
|
|
||||||
if paths is None:
|
|
||||||
if allowed_uses is not None:
|
|
||||||
raise PermissionError(
|
|
||||||
f"Direct access to module {module_id} is "
|
|
||||||
f"not allowed for {caller_module_id}. Specify allowed attributes."
|
|
||||||
)
|
|
||||||
return module
|
|
||||||
|
|
||||||
if isinstance(paths, str):
|
|
||||||
paths = [paths]
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
current_obj = module
|
|
||||||
try:
|
|
||||||
parts = path.split(".")
|
|
||||||
for part in parts:
|
|
||||||
if allowed_uses is not None and part not in allowed_uses:
|
|
||||||
raise AttributeError(
|
|
||||||
f"Access to '{part}' is not allowed "
|
|
||||||
+ f"for module {caller_module_id}"
|
|
||||||
)
|
|
||||||
current_obj = getattr(current_obj, part)
|
|
||||||
results.append(current_obj)
|
|
||||||
except AttributeError as e:
|
|
||||||
if "is not allowed" in str(e):
|
|
||||||
raise PermissionError(str(e))
|
|
||||||
results.append(None)
|
|
||||||
|
|
||||||
if len(results) == 1:
|
|
||||||
return results[0]
|
|
||||||
else:
|
|
||||||
return tuple(results)
|
|
||||||
|
|
||||||
|
|
||||||
class Storage:
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set(key: str, value: Any):
|
|
||||||
storage = Singleton().storage
|
|
||||||
storage[key] = value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get(key: str):
|
|
||||||
storage = Singleton().storage
|
|
||||||
return storage.get(key)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
CLEAN_HTML = re.compile("<.*?>")
|
|
||||||
|
|
||||||
|
|
||||||
class Utils:
|
|
||||||
@staticmethod
|
|
||||||
def code_format(code: str, lang: str):
|
|
||||||
if lang:
|
|
||||||
return f'<pre><code class="language-{lang}">{code}</code></pre>'
|
|
||||||
else:
|
|
||||||
return f"<pre>{code}</pre>"
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import types
|
|
||||||
from _ast import AnnAssign
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from aiogram import Bot
|
|
||||||
from RestrictedPython import (
|
|
||||||
RestrictingNodeTransformer,
|
|
||||||
limited_builtins,
|
|
||||||
safe_builtins,
|
|
||||||
utility_builtins,
|
|
||||||
)
|
|
||||||
from RestrictedPython.Eval import default_guarded_getitem, default_guarded_getiter
|
|
||||||
from RestrictedPython.Guards import ( # guarded_setattr,; full_write_guard,
|
|
||||||
_write_wrapper,
|
|
||||||
guarded_unpack_sequence,
|
|
||||||
safer_getattr,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ocab_core.logger import log
|
|
||||||
from ocab_core.modules_system.safe.zope_guards import extra_safe_builtins
|
|
||||||
|
|
||||||
|
|
||||||
class RestrictedPythonPolicy(RestrictingNodeTransformer):
|
|
||||||
def visit_AsyncFunctionDef(self, node):
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
def visit_Await(self, node):
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
def visit_AsyncFor(self, node):
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
def visit_AsyncWith(self, node):
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Не работает из-за getattr
|
|
||||||
|
|
||||||
def visit_Match(self, node) -> Any:
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
def visit_match_case(self, node) -> Any:
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
def visit_MatchAs(self, node) -> Any:
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
def visit_MatchValue(self, node) -> Any:
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def visit_AnnAssign(self, node: AnnAssign) -> Any:
|
|
||||||
# missing in RestrictingNodeTransformer
|
|
||||||
# this doesn't need the logic that is in visit_Assign
|
|
||||||
# because it doesn't have a "targets" attribute,
|
|
||||||
# and node.target: Name | Attribute | Subscript
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
# new Python 3.12 nodes
|
|
||||||
def visit_TypeAlias(self, node) -> Any:
|
|
||||||
# missing in RestrictingNodeTransformer
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
def visit_TypeVar(self, node) -> Any:
|
|
||||||
# missing in RestrictingNodeTransformer
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
def visit_TypeVarTuple(self, node) -> Any:
|
|
||||||
# missing in RestrictingNodeTransformer
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
def visit_ParamSpec(self, node) -> Any:
|
|
||||||
# missing in RestrictingNodeTransformer
|
|
||||||
return self.node_contents_visit(node)
|
|
||||||
|
|
||||||
|
|
||||||
def _metaclass(name, bases, dict):
|
|
||||||
ob = type(name, bases, dict)
|
|
||||||
ob.__allow_access_to_unprotected_subobjects__ = 1
|
|
||||||
ob._guarded_writes = 1
|
|
||||||
return ob
|
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_IMPORTS = [
|
|
||||||
"typing",
|
|
||||||
"aiogram",
|
|
||||||
"warnings",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def safes_getattr(object, name, default=None, getattr=safer_getattr):
|
|
||||||
if isinstance(object, Bot) and name == "token":
|
|
||||||
log("Bot.token is not allowed")
|
|
||||||
raise Exception("Bot.token is not allowed")
|
|
||||||
|
|
||||||
return getattr(object, name, default)
|
|
||||||
|
|
||||||
|
|
||||||
trusted_settters_classes = []
|
|
||||||
|
|
||||||
|
|
||||||
def safes_setattr(self, key, value):
|
|
||||||
if (
|
|
||||||
isinstance(getattr(type(self), key, None), property)
|
|
||||||
and getattr(type(self), key).fset is not None
|
|
||||||
):
|
|
||||||
getattr(type(self), key).fset(self, value)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def write_guard():
|
|
||||||
# ed scope abuse!
|
|
||||||
# safetypes and Wrapper variables are used by guard()
|
|
||||||
safetypes = {dict, list}
|
|
||||||
Wrapper = _write_wrapper()
|
|
||||||
|
|
||||||
def guard(ob):
|
|
||||||
# Don't bother wrapping simple types, or objects that claim to
|
|
||||||
# handle their own write security.
|
|
||||||
if type(ob) in safetypes or hasattr(ob, "_guarded_writes"):
|
|
||||||
return ob
|
|
||||||
|
|
||||||
if type(ob) in trusted_settters_classes:
|
|
||||||
setattr(ob, "__guarded_setattr__", types.MethodType(safes_setattr, ob))
|
|
||||||
|
|
||||||
# Hand the object to the Wrapper instance, then return the instance.
|
|
||||||
return Wrapper(ob)
|
|
||||||
|
|
||||||
return guard
|
|
||||||
|
|
||||||
|
|
||||||
BUILTINS = safe_builtins.copy()
|
|
||||||
BUILTINS.update(utility_builtins)
|
|
||||||
BUILTINS.update(limited_builtins)
|
|
||||||
BUILTINS.update(extra_safe_builtins)
|
|
||||||
BUILTINS["__metaclass__"] = _metaclass
|
|
||||||
BUILTINS["_getitem_"] = default_guarded_getitem
|
|
||||||
BUILTINS["_getattr_"] = safes_getattr
|
|
||||||
BUILTINS["_getiter_"] = default_guarded_getiter
|
|
||||||
BUILTINS["_write_"] = write_guard()
|
|
||||||
BUILTINS["_unpack_sequence_"] = guarded_unpack_sequence
|
|
||||||
BUILTINS["staticmethod"] = staticmethod
|
|
||||||
BUILTINS["tuple"] = tuple
|
|
||||||
BUILTINS["reversed"] = reversed
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
#############################################################################
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 OCAB Team
|
|
||||||
# Copyright (c) 2002 Zope Foundation and Contributors.
|
|
||||||
#
|
|
||||||
# This software includes a function derived from the software subject to the
|
|
||||||
# provisions of the Zope Public License, Version 2.1 (ZPL). A copy of the ZPL
|
|
||||||
# should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
|
|
||||||
# AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT
|
|
||||||
# LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST
|
|
||||||
# INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
extra_safe_builtins = {}
|
|
||||||
|
|
||||||
|
|
||||||
class GuardedDictType:
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
return dict(*args, **kwargs)
|
|
||||||
|
|
||||||
def fromkeys(self, S, v=None):
|
|
||||||
return dict.fromkeys(S, v)
|
|
||||||
|
|
||||||
|
|
||||||
extra_safe_builtins["dict"] = GuardedDictType()
|
|
||||||
|
|
||||||
|
|
||||||
ContainerAssertions = {
|
|
||||||
type(()): 1,
|
|
||||||
bytes: 1,
|
|
||||||
str: 1,
|
|
||||||
range: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
Containers = ContainerAssertions.get
|
|
||||||
|
|
||||||
|
|
||||||
def _error(index):
|
|
||||||
raise Exception("unauthorized access to element")
|
|
||||||
|
|
||||||
|
|
||||||
def guard(container, value, index=None):
|
|
||||||
# if Containers(type(container)) and Containers(type(value)):
|
|
||||||
# # Simple type. Short circuit.
|
|
||||||
# return
|
|
||||||
# I don't know how to do this.
|
|
||||||
# if getSecurityManager().validate(container, container, index, value):
|
|
||||||
# return
|
|
||||||
# _error(index)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
class SafeIter:
|
|
||||||
__allow_access_to_unprotected_subobjects__ = 1
|
|
||||||
|
|
||||||
def __init__(self, ob, container=None):
|
|
||||||
self._iter = iter(ob)
|
|
||||||
if container is None:
|
|
||||||
container = ob
|
|
||||||
self.container = container
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __next__(self):
|
|
||||||
ob = next(self._iter)
|
|
||||||
guard(self.container, ob)
|
|
||||||
return ob
|
|
||||||
|
|
||||||
next = __next__
|
|
||||||
|
|
||||||
|
|
||||||
class NullIter(SafeIter):
|
|
||||||
def __init__(self, ob):
|
|
||||||
self._iter = ob
|
|
||||||
|
|
||||||
def __next__(self):
|
|
||||||
return next(self._iter)
|
|
||||||
|
|
||||||
next = __next__
|
|
||||||
|
|
||||||
|
|
||||||
def guarded_iter(*args):
|
|
||||||
if len(args) == 1:
|
|
||||||
i = args[0]
|
|
||||||
# Don't double-wrap
|
|
||||||
if isinstance(i, SafeIter):
|
|
||||||
return i
|
|
||||||
if not isinstance(i, range):
|
|
||||||
return SafeIter(i)
|
|
||||||
# Other call styles / targets don't need to be guarded
|
|
||||||
return NullIter(iter(*args))
|
|
||||||
|
|
||||||
|
|
||||||
extra_safe_builtins["iter"] = guarded_iter
|
|
||||||
|
|
||||||
|
|
||||||
def guarded_any(seq):
|
|
||||||
return any(guarded_iter(seq))
|
|
||||||
|
|
||||||
|
|
||||||
extra_safe_builtins["any"] = guarded_any
|
|
||||||
|
|
||||||
|
|
||||||
def guarded_all(seq):
|
|
||||||
return all(guarded_iter(seq))
|
|
||||||
|
|
||||||
|
|
||||||
extra_safe_builtins["all"] = guarded_all
|
|
||||||
|
|
||||||
valid_inplace_types = (list, set)
|
|
||||||
|
|
||||||
inplace_slots = {
|
|
||||||
"+=": "__iadd__",
|
|
||||||
"-=": "__isub__",
|
|
||||||
"*=": "__imul__",
|
|
||||||
"/=": (1 / 2 == 0) and "__idiv__" or "__itruediv__",
|
|
||||||
"//=": "__ifloordiv__",
|
|
||||||
"%=": "__imod__",
|
|
||||||
"**=": "__ipow__",
|
|
||||||
"<<=": "__ilshift__",
|
|
||||||
">>=": "__irshift__",
|
|
||||||
"&=": "__iand__",
|
|
||||||
"^=": "__ixor__",
|
|
||||||
"|=": "__ior__",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def __iadd__(x, y):
|
|
||||||
x += y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def __isub__(x, y):
|
|
||||||
x -= y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def __imul__(x, y):
|
|
||||||
x *= y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def __idiv__(x, y):
|
|
||||||
x /= y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def __ifloordiv__(x, y):
|
|
||||||
x //= y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def __imod__(x, y):
|
|
||||||
x %= y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def __ipow__(x, y):
|
|
||||||
x **= y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def __ilshift__(x, y):
|
|
||||||
x <<= y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def __irshift__(x, y):
|
|
||||||
x >>= y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def __iand__(x, y):
|
|
||||||
x &= y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def __ixor__(x, y):
|
|
||||||
x ^= y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def __ior__(x, y):
|
|
||||||
x |= y
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
inplace_ops = {
|
|
||||||
"+=": __iadd__,
|
|
||||||
"-=": __isub__,
|
|
||||||
"*=": __imul__,
|
|
||||||
"/=": __idiv__,
|
|
||||||
"//=": __ifloordiv__,
|
|
||||||
"%=": __imod__,
|
|
||||||
"**=": __ipow__,
|
|
||||||
"<<=": __ilshift__,
|
|
||||||
">>=": __irshift__,
|
|
||||||
"&=": __iand__,
|
|
||||||
"^=": __ixor__,
|
|
||||||
"|=": __ior__,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def protected_inplacevar(op, var, expr):
|
|
||||||
"""Do an inplace operation
|
|
||||||
|
|
||||||
If the var has an inplace slot, then disallow the operation
|
|
||||||
unless the var an instance of ``valid_inplace_types``.
|
|
||||||
"""
|
|
||||||
if hasattr(var, inplace_slots[op]) and not isinstance(var, valid_inplace_types):
|
|
||||||
try:
|
|
||||||
cls = var.__class__
|
|
||||||
except AttributeError:
|
|
||||||
cls = type(var)
|
|
||||||
raise TypeError(
|
|
||||||
"Augmented assignment to %s objects is not allowed"
|
|
||||||
" in untrusted code" % cls.__name__
|
|
||||||
)
|
|
||||||
return inplace_ops[op](var, expr)
|
|
||||||
|
|
||||||
|
|
||||||
extra_safe_builtins["_inplacevar_"] = protected_inplacevar
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
from aiogram import Bot, Dispatcher
|
|
||||||
from aiogram.fsm.storage.memory import MemoryStorage
|
|
||||||
|
|
||||||
from ocab_core.modules_system import ModulesManager
|
|
||||||
|
|
||||||
|
|
||||||
class SingletonMeta(type):
|
|
||||||
_instances = {}
|
|
||||||
|
|
||||||
def __call__(cls, *args, **kwargs):
|
|
||||||
if cls not in cls._instances:
|
|
||||||
instance = super().__call__(*args, **kwargs)
|
|
||||||
cls._instances[cls] = instance
|
|
||||||
return cls._instances[cls]
|
|
||||||
|
|
||||||
|
|
||||||
class Singleton(metaclass=SingletonMeta):
|
|
||||||
bot: Bot
|
|
||||||
dp: Dispatcher = None
|
|
||||||
modules_manager: ModulesManager = None
|
|
||||||
storage = {
|
|
||||||
"_fsm_storage": MemoryStorage(),
|
|
||||||
"_routers": [],
|
|
||||||
"_outer_message_middlewares": [],
|
|
||||||
}
|
|
||||||
2140
src/ocab_core/poetry.lock
generated
2140
src/ocab_core/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
|||||||
[virtualenvs]
|
|
||||||
in-project = true
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
[tool.poetry]
|
|
||||||
name = "ocab-core"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = ""
|
|
||||||
authors = ["Максим Слипенко <maxim@slipenko.com>"]
|
|
||||||
readme = "README.md"
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "~3.12"
|
|
||||||
aiogram = "^3.10.0"
|
|
||||||
setuptools = "^71.0.1"
|
|
||||||
restrictedpython = "^7.1"
|
|
||||||
semver = "^3.0.2"
|
|
||||||
dataclasses-json = "^0.6.7"
|
|
||||||
fastapi = { version = "^0.111.1", optional = true }
|
|
||||||
hypercorn = { version = "^0.17.3", optional = true }
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
ocab-modules = { path = "../ocab_modules", develop = true }
|
|
||||||
|
|
||||||
[tool.poetry.extras]
|
|
||||||
webhook = ["fastapi", "hypercorn"]
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# OCAB Modules
|
|
||||||
|
|
||||||
OCAB Modules содержит набор модулей для платформы Open Chat AI Bot (OCAB).
|
|
||||||
|
|
||||||
## Описание
|
|
||||||
|
|
||||||
OCAB - это платформа для создания чат-ботов Telegram. Модули - это расширения, которые добавляют функциональность ботам OCAB.
|
|
||||||
|
|
||||||
## Типы модулей
|
|
||||||
|
|
||||||
* **Стандартные модули (standard.*):** Предоставляют основные функции, такие как управление пользователями, ролями и настройками.
|
|
||||||
* **Дополнительные официальные модули (external.*):** Разработаны командой OCAB и предоставляют расширенные возможности, такие как интеграция с нейросетями, внешними сервисами и API.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .lib import module_loader
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import yandexgpt
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Модуль Create Report Apps
|
|
||||||
|
|
||||||
Модуль `create_report_apps` предназначен для помощи пользователям в создании отчетов об ошибках в приложениях.
|
|
||||||
|
|
||||||
## Функциональность
|
|
||||||
|
|
||||||
- Задает пользователю ряд вопросов, необходимых для составления отчета.
|
|
||||||
- Собирает информацию о системе пользователя.
|
|
||||||
- Формирует отчет в текстовом формате.
|
|
||||||
|
|
||||||
## Команды
|
|
||||||
|
|
||||||
- `/create_report_apps` - запустить процесс создания отчета.
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
1. Отправьте команду `/create_report_apps` боту в личных сообщениях или в групповом чате.
|
|
||||||
2. Ответьте на вопросы бота.
|
|
||||||
3. Бот сформирует отчет и отправит его вам.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .main import module_init
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
from aiogram import Bot, Router
|
|
||||||
from aiogram.enums import ParseMode
|
|
||||||
from aiogram.fsm.context import FSMContext
|
|
||||||
from aiogram.fsm.state import State, StatesGroup
|
|
||||||
from aiogram.types import (
|
|
||||||
BufferedInputFile,
|
|
||||||
KeyboardButton,
|
|
||||||
Message,
|
|
||||||
ReplyKeyboardMarkup,
|
|
||||||
ReplyKeyboardRemove,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ocab_core.modules_system.public_api import Utils, get_fsm_context
|
|
||||||
|
|
||||||
from .report import Report
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
|
|
||||||
class ReportState(StatesGroup):
|
|
||||||
input_system_info = State()
|
|
||||||
input_app_name = State()
|
|
||||||
input_problem_step_by_step = State()
|
|
||||||
input_actual_result = State()
|
|
||||||
input_expected_result = State()
|
|
||||||
input_additional_info = State()
|
|
||||||
|
|
||||||
|
|
||||||
system_info_code = """echo "SESSION_TYPE: ${XDG_SESSION_TYPE:-Unknown}"
|
|
||||||
[ -f /etc/os-release ] && grep "^PRETTY_NAME=" /etc/os-release | cut -d= -f2 \
|
|
||||||
| tr -d '"' | xargs echo "OS: "
|
|
||||||
echo "Kernel: $(uname -r)"
|
|
||||||
echo "DE: ${XDG_CURRENT_DESKTOP:-Unknown}"
|
|
||||||
grep "^model name" /proc/cpuinfo | head -n1 | cut -d: -f2 \
|
|
||||||
| xargs echo "CPU: "
|
|
||||||
lspci | grep "VGA compatible controller" | cut -d: -f3 \
|
|
||||||
| xargs -I{} echo "GPU: {}"
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
system_info_message = """Укажите параметры свой системы.
|
|
||||||
Собрать информацию о системе можно с помощью данного скрипта:
|
|
||||||
""" + Utils.code_format(
|
|
||||||
system_info_code,
|
|
||||||
"shell",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def start_report(chat_id: int, bot: Bot):
|
|
||||||
await bot.send_message(
|
|
||||||
chat_id=chat_id,
|
|
||||||
text=system_info_message,
|
|
||||||
parse_mode=ParseMode.HTML,
|
|
||||||
reply_markup=ReplyKeyboardRemove(),
|
|
||||||
)
|
|
||||||
state = await get_fsm_context(chat_id, chat_id)
|
|
||||||
|
|
||||||
await state.set_state(ReportState.input_system_info)
|
|
||||||
|
|
||||||
|
|
||||||
app_info_message = """Укажите название и версию приложения.
|
|
||||||
Узнать можно с помощью данной команды:""" + Utils.code_format(
|
|
||||||
"rpm -qa | grep -i НАЗВАНИЕ_ПРИЛОЖЕНИЯ", "shell"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(ReportState.input_system_info)
|
|
||||||
async def system_entered(message: Message, state: FSMContext):
|
|
||||||
await state.update_data(system=message.text)
|
|
||||||
await message.answer(
|
|
||||||
text=app_info_message,
|
|
||||||
parse_mode=ParseMode.HTML,
|
|
||||||
)
|
|
||||||
await state.set_state(ReportState.input_app_name)
|
|
||||||
|
|
||||||
|
|
||||||
step_by_step_message = (
|
|
||||||
"""Опиши проблему пошагово, что ты делал, что происходило, что не так."""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(ReportState.input_app_name)
|
|
||||||
async def app_name_entered(message: Message, state: FSMContext):
|
|
||||||
await state.update_data(app=message.text)
|
|
||||||
await message.answer(text=step_by_step_message)
|
|
||||||
await state.set_state(ReportState.input_problem_step_by_step)
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(ReportState.input_problem_step_by_step)
|
|
||||||
async def problem_step_by_step_entered(message: Message, state: FSMContext):
|
|
||||||
await state.update_data(problem_step_by_step=message.text)
|
|
||||||
await message.answer(text="Опиши, что произошло (фактический результат).")
|
|
||||||
await state.set_state(ReportState.input_actual_result)
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(ReportState.input_actual_result)
|
|
||||||
async def actual_result_entered(message: Message, state: FSMContext):
|
|
||||||
await state.update_data(actual=message.text)
|
|
||||||
await message.answer(text="Опиши ожидаемый результат.")
|
|
||||||
await state.set_state(ReportState.input_expected_result)
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(ReportState.input_expected_result)
|
|
||||||
async def expected_result_entered(message: Message, state: FSMContext):
|
|
||||||
await state.update_data(expected=message.text)
|
|
||||||
await message.answer(
|
|
||||||
text="Если есть дополнительная информация, то напиши ее.",
|
|
||||||
reply_markup=ReplyKeyboardMarkup(
|
|
||||||
resize_keyboard=True,
|
|
||||||
keyboard=[
|
|
||||||
[KeyboardButton(text="Дополнительной информации нет")],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
await state.set_state(ReportState.input_additional_info)
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(ReportState.input_additional_info)
|
|
||||||
async def additional_info_entered(message: Message, state: FSMContext):
|
|
||||||
if message.text == "Дополнительной информации нет":
|
|
||||||
additional_info = ""
|
|
||||||
else:
|
|
||||||
additional_info = message.text
|
|
||||||
await state.update_data(additional=additional_info)
|
|
||||||
await message.answer(
|
|
||||||
text="Вот твой отчет сообщением, а также файлом:",
|
|
||||||
reply_markup=ReplyKeyboardRemove(),
|
|
||||||
)
|
|
||||||
data = await state.get_data()
|
|
||||||
|
|
||||||
report = Report(data)
|
|
||||||
file_report = report.export().encode()
|
|
||||||
|
|
||||||
await message.answer(text=report.export())
|
|
||||||
await message.answer_document(document=BufferedInputFile(file_report, "report.txt"))
|
|
||||||
await state.clear()
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "external.create_report_apps",
|
|
||||||
"name": "Create Report Apps",
|
|
||||||
"description": "Модуль для создания отчетов о ошибках в приложениях",
|
|
||||||
"author": [
|
|
||||||
"OCAB Team",
|
|
||||||
"Maxim Slipenko"
|
|
||||||
],
|
|
||||||
"version": "1.0.0",
|
|
||||||
"privileged": false,
|
|
||||||
"dependencies": {
|
|
||||||
"standard.command_helper": "^1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
from typing import Union
|
|
||||||
|
|
||||||
from aiogram import Bot, F, Router
|
|
||||||
from aiogram.exceptions import TelegramForbiddenError
|
|
||||||
from aiogram.filters import BaseFilter, Command, CommandStart
|
|
||||||
from aiogram.types import (
|
|
||||||
CallbackQuery,
|
|
||||||
InlineKeyboardButton,
|
|
||||||
InlineKeyboardMarkup,
|
|
||||||
Message,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ocab_core.modules_system.public_api import get_module, register_router
|
|
||||||
|
|
||||||
from .create_report import router as create_report_router
|
|
||||||
from .create_report import start_report
|
|
||||||
|
|
||||||
register_command = get_module("standard.command_helper", "register_command")
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
|
|
||||||
class ChatTypeFilter(BaseFilter):
|
|
||||||
def __init__(self, chat_type: Union[str, list]):
|
|
||||||
self.chat_type = chat_type
|
|
||||||
|
|
||||||
async def __call__(self, message: Message) -> bool:
|
|
||||||
if isinstance(self.chat_type, str):
|
|
||||||
return message.chat.type == self.chat_type
|
|
||||||
return message.chat.type in self.chat_type
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(
|
|
||||||
ChatTypeFilter(chat_type=["group", "supergroup"]), Command("create_report_apps")
|
|
||||||
)
|
|
||||||
async def create_report_apps_command_group(message: Message):
|
|
||||||
keyboard = InlineKeyboardMarkup(
|
|
||||||
inline_keyboard=[
|
|
||||||
[
|
|
||||||
InlineKeyboardButton(
|
|
||||||
text="Да", callback_data=f"create_report:{message.from_user.id}"
|
|
||||||
),
|
|
||||||
InlineKeyboardButton(
|
|
||||||
text="Нет", callback_data=f"cancel_report:{message.from_user.id}"
|
|
||||||
),
|
|
||||||
]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
await message.answer(
|
|
||||||
"Я могу отправить тебе пару вопросов "
|
|
||||||
"для помощи в составлении репорта личными "
|
|
||||||
"сообщениями.",
|
|
||||||
reply_markup=keyboard,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(
|
|
||||||
ChatTypeFilter(chat_type=["private"]),
|
|
||||||
CommandStart(deep_link=True, magic=F.args == "create_report_apps"),
|
|
||||||
)
|
|
||||||
@router.message(ChatTypeFilter(chat_type=["private"]), Command("create_report_apps"))
|
|
||||||
async def create_report_apps_command(message: Message, bot: Bot):
|
|
||||||
await start_report(message.from_user.id, bot)
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data.startswith("cancel_report"))
|
|
||||||
async def cancel_report_callback(callback_query: CallbackQuery):
|
|
||||||
callback_user_id = int(callback_query.data.split(":")[1])
|
|
||||||
if callback_query.from_user.id != callback_user_id:
|
|
||||||
await callback_query.answer("Эта кнопка не для вас.", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
await callback_query.message.delete()
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data.startswith("create_report"))
|
|
||||||
async def create_report_callback(callback_query: CallbackQuery, bot: Bot):
|
|
||||||
callback_user_id = int(callback_query.data.split(":")[1])
|
|
||||||
if callback_query.from_user.id != callback_user_id:
|
|
||||||
await callback_query.answer("Эта кнопка не для вас.", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
user_id = callback_query.from_user.id
|
|
||||||
|
|
||||||
async def on_chat_unavailable():
|
|
||||||
await callback_query.message.edit_text(
|
|
||||||
"Я в личных сообщениях задам тебе вопросы "
|
|
||||||
"для помощи в составлении репорта. "
|
|
||||||
'Но перед этим ты должен нажать кнопку "Запустить"'
|
|
||||||
)
|
|
||||||
info = await bot.get_me()
|
|
||||||
await callback_query.answer(
|
|
||||||
url=f"https://t.me/{info.username}?start=create_report_apps"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
chat_member = await bot.get_chat_member(chat_id=user_id, user_id=user_id)
|
|
||||||
if chat_member.status != "left":
|
|
||||||
await start_report(user_id, bot)
|
|
||||||
await callback_query.message.edit_text(
|
|
||||||
"Я в личных сообщениях задам тебе "
|
|
||||||
"вопросы для помощи в составлении "
|
|
||||||
"репорта."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await on_chat_unavailable()
|
|
||||||
except TelegramForbiddenError:
|
|
||||||
await on_chat_unavailable()
|
|
||||||
|
|
||||||
|
|
||||||
async def module_init():
|
|
||||||
router.include_router(create_report_router)
|
|
||||||
|
|
||||||
register_router(router)
|
|
||||||
register_command("create_report_apps", "Написать репорт о приложении")
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import aiogram
|
|
||||||
|
|
||||||
|
|
||||||
class ReportFormatter:
|
|
||||||
def __init__(self, html=True):
|
|
||||||
self.html = html
|
|
||||||
|
|
||||||
def bold(self, string):
|
|
||||||
if self.html:
|
|
||||||
return f"<b>{self.text(string)}</b>"
|
|
||||||
return self.text(string)
|
|
||||||
|
|
||||||
def text(self, string):
|
|
||||||
if self.html:
|
|
||||||
return aiogram.html.quote(string)
|
|
||||||
return string
|
|
||||||
|
|
||||||
|
|
||||||
class Report:
|
|
||||||
def __init__(self, data: dict):
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
def export(self):
|
|
||||||
data = self.data
|
|
||||||
|
|
||||||
report = f"""
|
|
||||||
Стенд с ошибкой:
|
|
||||||
==============================
|
|
||||||
|
|
||||||
{data['system']}
|
|
||||||
|
|
||||||
Пакет:
|
|
||||||
==============================
|
|
||||||
|
|
||||||
{data['app']}
|
|
||||||
|
|
||||||
Шаги, приводящие к ошибке:
|
|
||||||
==============================
|
|
||||||
|
|
||||||
{data['problem_step_by_step']}
|
|
||||||
|
|
||||||
Фактический результат:
|
|
||||||
==============================
|
|
||||||
|
|
||||||
{data['actual']}
|
|
||||||
|
|
||||||
Ожидаемый результат:
|
|
||||||
==============================
|
|
||||||
|
|
||||||
{data['expected']}
|
|
||||||
"""
|
|
||||||
if data["additional"] != "":
|
|
||||||
report += f"""
|
|
||||||
Дополнительно:
|
|
||||||
==============================
|
|
||||||
|
|
||||||
{data['additional']}
|
|
||||||
"""
|
|
||||||
return report
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Модуль 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,2 +0,0 @@
|
|||||||
from .handlers import answer_to_message
|
|
||||||
from .main import module_init
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# 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, 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",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
# print("answer_to_message")
|
|
||||||
log("answer_to_message")
|
|
||||||
yagpt = YandexGPT(get_yandexgpt_token(), get_yandexgpt_catalog_id())
|
|
||||||
text = message.text
|
|
||||||
prompt = get_yandexgpt_prompt()
|
|
||||||
# response = await yagpt.async_yandexgpt(system_prompt=prompt, input_messages=text)
|
|
||||||
response = await yagpt.yandexgpt_request(
|
|
||||||
chat_id=message.chat.id, message_id=message.message_id, type="yandexgpt"
|
|
||||||
)
|
|
||||||
reply = await message.reply(response, parse_mode="Markdown")
|
|
||||||
add_message(reply, message_ai_model="yandexgpt")
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"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": "*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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,10 +0,0 @@
|
|||||||
# flake8: noqa
|
|
||||||
from aiogram import F, Router
|
|
||||||
|
|
||||||
from .handlers import answer_to_message
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
# Если сообщение содержит в начале текст "Гномик" или "гномик" или отвечает на сообщение бота, то вызывается функция answer_to_message
|
|
||||||
router.message.register(
|
|
||||||
answer_to_message, F.text.startswith("Гномик") | F.text.startswith("гномик")
|
|
||||||
)
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
# flake8: noqa
|
|
||||||
import json
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from ocab_core.modules_system.public_api import get_module, log
|
|
||||||
|
|
||||||
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:
|
|
||||||
token = None
|
|
||||||
catalog_id = None
|
|
||||||
languages = {
|
|
||||||
"ru": "русский язык",
|
|
||||||
"en": "английский язык",
|
|
||||||
"de": "немецкий язык",
|
|
||||||
"uk": "украинский язык",
|
|
||||||
"es": "испанский язык",
|
|
||||||
"be": "белорусский язык",
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, token, catalog_id):
|
|
||||||
self.token = token
|
|
||||||
self.catalog_id = catalog_id
|
|
||||||
|
|
||||||
async def async_request(self, url, headers, prompt) -> dict:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(url, headers=headers, json=prompt) as response:
|
|
||||||
return await response.json()
|
|
||||||
|
|
||||||
async def async_token_check(
|
|
||||||
self, messages, gpt, max_tokens, stream, temperature, del_msg_id=1
|
|
||||||
):
|
|
||||||
url = "https://llm.api.cloud.yandex.net/foundationModels/v1/tokenizeCompletion"
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Api-Key {self.token}",
|
|
||||||
}
|
|
||||||
answer_token = get_yandexgpt_token_for_answer()
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
request = {
|
|
||||||
"modelUri": gpt,
|
|
||||||
"completionOptions": {
|
|
||||||
"stream": stream,
|
|
||||||
"temperature": temperature,
|
|
||||||
"maxTokens": max_tokens,
|
|
||||||
},
|
|
||||||
"messages": messages,
|
|
||||||
}
|
|
||||||
response = await self.async_request(
|
|
||||||
url=url, headers=headers, prompt=request
|
|
||||||
)
|
|
||||||
except Exception as e: # TODO: Переделать обработку ошибок
|
|
||||||
# print(e)
|
|
||||||
log(f"Error: {e}")
|
|
||||||
|
|
||||||
continue
|
|
||||||
if int(len(response["tokens"])) < (max_tokens - answer_token):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
messages.pop(del_msg_id)
|
|
||||||
except IndexError:
|
|
||||||
Exception("IndexError: list index out of range")
|
|
||||||
return messages
|
|
||||||
|
|
||||||
async def async_yandexgpt_lite(
|
|
||||||
self,
|
|
||||||
system_prompt,
|
|
||||||
input_messages,
|
|
||||||
stream=False,
|
|
||||||
temperature=0.6,
|
|
||||||
max_tokens=8000,
|
|
||||||
):
|
|
||||||
url = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"
|
|
||||||
gpt = f"gpt://{self.catalog_id}/yandexgpt-lite/latest"
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Api-Key {self.token}",
|
|
||||||
}
|
|
||||||
|
|
||||||
messages = [{"role": "system", "text": system_prompt}]
|
|
||||||
for message in input_messages:
|
|
||||||
messages.append(message)
|
|
||||||
messages = await self.async_token_check(messages, gpt, max_tokens)
|
|
||||||
|
|
||||||
prompt = {
|
|
||||||
"modelUri": gpt,
|
|
||||||
"completionOptions": {
|
|
||||||
"stream": stream,
|
|
||||||
"temperature": temperature,
|
|
||||||
"maxTokens": max_tokens,
|
|
||||||
},
|
|
||||||
"messages": messages,
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(url, headers=headers, json=prompt).text # nosec
|
|
||||||
return json.loads(response)["result"]["alternatives"][0]["message"]["text"]
|
|
||||||
|
|
||||||
async def async_yandexgpt(
|
|
||||||
self,
|
|
||||||
system_prompt,
|
|
||||||
input_messages,
|
|
||||||
stream=False,
|
|
||||||
temperature=0.6,
|
|
||||||
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 = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Api-Key {self.token}",
|
|
||||||
}
|
|
||||||
|
|
||||||
messages = []
|
|
||||||
messages.append({"role": "system", "text": system_prompt})
|
|
||||||
for message in input_messages:
|
|
||||||
messages.append(message)
|
|
||||||
|
|
||||||
messages = await self.async_token_check(
|
|
||||||
messages, gpt, max_tokens, stream, temperature
|
|
||||||
)
|
|
||||||
|
|
||||||
request = {
|
|
||||||
"modelUri": gpt,
|
|
||||||
"completionOptions": {
|
|
||||||
"stream": stream,
|
|
||||||
"temperature": temperature,
|
|
||||||
"maxTokens": max_tokens,
|
|
||||||
},
|
|
||||||
"messages": messages,
|
|
||||||
}
|
|
||||||
response = await self.async_request(
|
|
||||||
url=url, headers=headers, prompt=request
|
|
||||||
) # nosec
|
|
||||||
return response["result"]["alternatives"][0]["message"]["text"]
|
|
||||||
|
|
||||||
async def async_yandexgpt_translate(self, input_language, output_language, text):
|
|
||||||
input_language = self.languages[input_language]
|
|
||||||
output_language = self.languages[output_language]
|
|
||||||
|
|
||||||
return await self.async_yandexgpt(
|
|
||||||
f"Переведи на {output_language} сохранив оригинальный смысл текста. Верни только результат:",
|
|
||||||
[{"role": "user", "text": text}],
|
|
||||||
stream=False,
|
|
||||||
temperature=0.6,
|
|
||||||
max_tokens=8000,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_yandexgpt_spelling_check(self, input_language, text):
|
|
||||||
input_language = self.languages[input_language]
|
|
||||||
|
|
||||||
return await self.async_yandexgpt(
|
|
||||||
f"Проверьте орфографию и пунктуацию текста на {input_language}. Верни исправленный текст "
|
|
||||||
f"без смысловых искажений:",
|
|
||||||
[{"role": "user", "text": text}],
|
|
||||||
stream=False,
|
|
||||||
temperature=0.6,
|
|
||||||
max_tokens=8000,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_yandexgpt_text_history(
|
|
||||||
self, input_messages, stream=False, temperature=0.6, max_tokens=8000
|
|
||||||
):
|
|
||||||
url = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"
|
|
||||||
gpt = f"gpt://{self.catalog_id}/summarization/latest"
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Api-Key {self.token}",
|
|
||||||
}
|
|
||||||
|
|
||||||
messages = []
|
|
||||||
for message in input_messages:
|
|
||||||
messages.append(message)
|
|
||||||
messages = await self.async_token_check(messages, gpt, max_tokens, del_msg_id=0)
|
|
||||||
|
|
||||||
prompt = {
|
|
||||||
"modelUri": gpt,
|
|
||||||
"completionOptions": {
|
|
||||||
"stream": stream,
|
|
||||||
"temperature": temperature,
|
|
||||||
"maxTokens": max_tokens,
|
|
||||||
},
|
|
||||||
"messages": messages,
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(url, headers=headers, json=prompt).text # nosec
|
|
||||||
return json.loads(response)["result"]["alternatives"][0]["message"]["text"]
|
|
||||||
|
|
||||||
async def async_yandex_cloud_text_to_speech(
|
|
||||||
self, text, voice, emotion, speed, format, quality
|
|
||||||
):
|
|
||||||
tts = "tts.api.cloud.yandex.net/speech/v1/tts:synthesize"
|
|
||||||
# TODO: Сделать функцию TTS
|
|
||||||
return 0
|
|
||||||
|
|
||||||
async def async_yandex_cloud_vision(self, image, features, language):
|
|
||||||
# TODO: Сделать функцию Vision
|
|
||||||
return 0
|
|
||||||
|
|
||||||
async def collect_messages(self, message_id, chat_id):
|
|
||||||
messages = []
|
|
||||||
# Собираем цепочку сообщений в формате: [{"role": "user", "text": "<Имя_пользователя>: Привет!"},
|
|
||||||
# {"role": "assistant", "text": "Привет!"}]
|
|
||||||
while True:
|
|
||||||
message = db_api.get_message_text(chat_id, message_id)
|
|
||||||
if db_api.get_message_ai_model(chat_id, message_id) != None:
|
|
||||||
messages.append({"role": "assistant", "text": message})
|
|
||||||
else:
|
|
||||||
sender_name = db_api.get_user_name(
|
|
||||||
db_api.get_message_sender_id(chat_id, message_id)
|
|
||||||
)
|
|
||||||
messages.append({"role": "user", "text": sender_name + ": " + message})
|
|
||||||
message_id = db_api.get_answer_to_message_id(chat_id, message_id)
|
|
||||||
if message_id is None:
|
|
||||||
break
|
|
||||||
return list(reversed(messages))
|
|
||||||
|
|
||||||
async def collecting_messages_for_history(
|
|
||||||
self, start_message_id, end_message_id, chat_id
|
|
||||||
):
|
|
||||||
messages = []
|
|
||||||
# Собираем цепочку сообщений в формате: [{"role": "user", "text": "<Имя_пользователя>: Привет!"},
|
|
||||||
# {"role": "assistant", "text": "Привет!"}]
|
|
||||||
while True:
|
|
||||||
message = db_api.get_message_text(chat_id, start_message_id)
|
|
||||||
if db_api.get_message_ai_model(chat_id, start_message_id) != None:
|
|
||||||
messages.append({"role": "assistant", "text": message})
|
|
||||||
else:
|
|
||||||
sender_name = db_api.get_user_name(
|
|
||||||
db_api.get_message_sender_id(chat_id, start_message_id)
|
|
||||||
)
|
|
||||||
messages.append({"role": "user", "text": sender_name + ": " + message})
|
|
||||||
start_message_id -= 1
|
|
||||||
if start_message_id <= end_message_id:
|
|
||||||
break
|
|
||||||
return messages.reverse()
|
|
||||||
|
|
||||||
async def yandexgpt_request(
|
|
||||||
self,
|
|
||||||
message_id=None,
|
|
||||||
type="yandexgpt-lite",
|
|
||||||
chat_id=None,
|
|
||||||
message_id_end=None,
|
|
||||||
input_language=None,
|
|
||||||
output_language=None,
|
|
||||||
text=None,
|
|
||||||
):
|
|
||||||
if type == "yandexgpt-lite":
|
|
||||||
messages = await self.collect_messages(message_id, chat_id)
|
|
||||||
return await self.async_yandexgpt_lite(
|
|
||||||
system_prompt=get_yandexgpt_prompt(),
|
|
||||||
input_messages=messages,
|
|
||||||
stream=False,
|
|
||||||
temperature=0.6,
|
|
||||||
max_tokens=8000,
|
|
||||||
)
|
|
||||||
elif type == "yandexgpt":
|
|
||||||
# print("yandexgpt_request")
|
|
||||||
log("yandexgpt_request")
|
|
||||||
messages = await self.collect_messages(message_id, chat_id)
|
|
||||||
return await self.async_yandexgpt(
|
|
||||||
system_prompt=get_yandexgpt_prompt(),
|
|
||||||
input_messages=messages,
|
|
||||||
stream=False,
|
|
||||||
temperature=0.6,
|
|
||||||
max_tokens=get_yandexgpt_token_for_request(),
|
|
||||||
)
|
|
||||||
elif type == "yandexgpt-translate":
|
|
||||||
return await self.async_yandexgpt_translate(
|
|
||||||
input_language,
|
|
||||||
output_language,
|
|
||||||
text=db_api.get_message_text(chat_id, message_id),
|
|
||||||
)
|
|
||||||
elif type == "yandexgpt-spelling-check":
|
|
||||||
return await self.async_yandexgpt_spelling_check(
|
|
||||||
input_language, text=db_api.get_message_text(chat_id, message_id)
|
|
||||||
)
|
|
||||||
elif type == "yandexgpt-text-history":
|
|
||||||
messages = await self.collect_messages_for_history(
|
|
||||||
message_id, message_id_end, chat_id
|
|
||||||
)
|
|
||||||
return await self.async_yandexgpt_text_history(
|
|
||||||
messages=messages, stream=False, temperature=0.6, max_tokens=8000
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return "Ошибка: Неизвестный тип запроса | Error: Unknown request type"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .moderation import ban_user, unmute_user
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Moderation",
|
|
||||||
"description": "Moderation commands for OCAB",
|
|
||||||
"author": "OCAB Team",
|
|
||||||
"version": "1.0"
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
# flake8: noqa
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import time
|
|
||||||
|
|
||||||
import aiogram
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
from ocab_modules.standard.config.config import *
|
|
||||||
from ocab_modules.standard.roles.roles import *
|
|
||||||
|
|
||||||
|
|
||||||
class Moderation:
|
|
||||||
def __init__(self):
|
|
||||||
access_rights = get_access_rights()
|
|
||||||
bot_check_message = bool(self.access_rights["BOT_CHECK_MESSAGE"])
|
|
||||||
bot_ban_user = bool(self.access_rights["BOT_BAN_USER"])
|
|
||||||
bot_mute_user = bool(self.access_rights["BOT_MUTE_USER"])
|
|
||||||
moderator_rights = self.access_rights["MODERATOR_RIGHTS"]
|
|
||||||
ai_check_message = bool(self.access_rights["AI_CHECK_MESSAGE"])
|
|
||||||
beta_ai_check_message = bool(self.access_rights["BETA_AI_CHECK_MESSAGE"])
|
|
||||||
|
|
||||||
async def time_to_seconds(time):
|
|
||||||
# Конвертация текстового указания времени по типу 3h, 5m, 10s в минуты
|
|
||||||
if time[-1] == "d":
|
|
||||||
return int(time[:-1]) * 86400
|
|
||||||
elif time[-1] == "h":
|
|
||||||
return int(time[:-1]) * 3600
|
|
||||||
elif time[-1] == "m":
|
|
||||||
return int(time[:-1]) * 60
|
|
||||||
elif time[-1] == "s":
|
|
||||||
return int(time[:-1])
|
|
||||||
|
|
||||||
async def short_time_to_time(self, time):
|
|
||||||
# Конвертация времени в длинное название
|
|
||||||
if time[-1] == "d":
|
|
||||||
return str(f"{time[0:-1]} дней")
|
|
||||||
elif time[-1] == "h":
|
|
||||||
return str(f"{time[0:-1]} часов")
|
|
||||||
elif time[-1] == "m":
|
|
||||||
return str(f"{time[0:-1]} минут")
|
|
||||||
elif time[-1] == "s":
|
|
||||||
return str(f"{time[0:-1]} секунд")
|
|
||||||
|
|
||||||
async def delete_message(self, chat_id, message_id, bot: aiogram.Bot):
|
|
||||||
await bot.delete_message(chat_id, message_id)
|
|
||||||
|
|
||||||
async def ban_user(self, chat_id, user_id, bot: aiogram.Bot):
|
|
||||||
await bot.ban_chat_member(chat_id, user_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def mute_user(chat_id, user_id, time, bot: aiogram.Bot):
|
|
||||||
mutePermissions = {
|
|
||||||
"can_send_messages": False,
|
|
||||||
"can_send_audios": False,
|
|
||||||
"can_send_documents": False,
|
|
||||||
"can_send_photos": False,
|
|
||||||
"can_send_videos": False,
|
|
||||||
"can_send_video_notes": False,
|
|
||||||
"can_send_voice_notes": False,
|
|
||||||
"can_send_polls": False,
|
|
||||||
"can_send_other_messages": False,
|
|
||||||
"can_add_web_page_previews": False,
|
|
||||||
"can_change_info": False,
|
|
||||||
"can_invite_users": False,
|
|
||||||
"can_pin_messages": False,
|
|
||||||
"can_manage_topics": False,
|
|
||||||
}
|
|
||||||
end_time = time + int(time.time())
|
|
||||||
await bot.restrict_chat_member(
|
|
||||||
chat_id, user_id, until_date=end_time, **mutePermissions
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def unmute_user(chat_id, user_id, bot: aiogram.Bot):
|
|
||||||
await bot.restrict_chat_member(
|
|
||||||
chat_id, user_id, use_independent_chat_permissions=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def ban_user(chat_id, user_id, bot: aiogram.Bot):
|
|
||||||
await bot.ban_chat_member(chat_id, user_id)
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# flake8: noqa
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import random
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from aiogram import Bot
|
|
||||||
from aiogram.types import inline_keyboard_button as types
|
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|
||||||
|
|
||||||
from ocab_modules.legacy.moderation import ban_user, unmute_user
|
|
||||||
from src.ocab_modules.standard.config.config import get_telegram_check_bot
|
|
||||||
from src.ocab_modules.standard.database.db_api import *
|
|
||||||
|
|
||||||
|
|
||||||
async def create_math_task():
|
|
||||||
first_number = random.randint(1, 100) # nosec
|
|
||||||
second_number = random.randint(1, 100) # nosec
|
|
||||||
answer = first_number + second_number
|
|
||||||
fake_answers = []
|
|
||||||
for i in range(3):
|
|
||||||
diff = random.randint(1, 10) # nosec
|
|
||||||
diff_sign = random.choice(["+", "-"]) # nosec
|
|
||||||
fake_answers.append(answer + diff if diff_sign == "+" else answer - diff)
|
|
||||||
fake_answers.append(answer)
|
|
||||||
random.shuffle(fake_answers)
|
|
||||||
return [answer, first_number, second_number, fake_answers]
|
|
||||||
|
|
||||||
|
|
||||||
async def ban_user_timer(chat_id: int, user_id: int, time: int, bot: Bot):
|
|
||||||
await asyncio.sleep(time)
|
|
||||||
if get_user(user_id) is not None:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
await ban_user()
|
|
||||||
|
|
||||||
|
|
||||||
async def check_new_user(message: Message, bot: Bot):
|
|
||||||
print("check_new_user")
|
|
||||||
if get_telegram_check_bot():
|
|
||||||
# Проверяем наличие пользователя в базе данных
|
|
||||||
|
|
||||||
if get_user(message.from_user.id) is None:
|
|
||||||
# Выдаём пользователю ограничение на отправку сообщений на 3 минуты
|
|
||||||
ban_task = Thread(
|
|
||||||
target=ban_user_timer,
|
|
||||||
args=(message.chat.id, message.from_user.id, 180, bot),
|
|
||||||
)
|
|
||||||
ban_task.start()
|
|
||||||
# Создаём задачу с отложенным выполнением на 3 минуты
|
|
||||||
|
|
||||||
math_task = await create_math_task()
|
|
||||||
text = f"{math_task[1]} + {math_task[2]}"
|
|
||||||
builder = InlineKeyboardBuilder()
|
|
||||||
for answer in math_task[3]:
|
|
||||||
if answer == math_task[0]:
|
|
||||||
builder.add(
|
|
||||||
types.InlineKeyboardButton(
|
|
||||||
text=answer, callback_data=f"check_math_task_true"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
builder.add(
|
|
||||||
types.InlineKeyboardButton(
|
|
||||||
text=answer, callback_data=f"check_math_task_false"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await message.reply(
|
|
||||||
f"Приветствую, {message.from_user.first_name}!\n"
|
|
||||||
f"Для продолжения работы с ботом, пожалуйста, решите математический пример в течении 3х минут:\n"
|
|
||||||
f"*{text}*",
|
|
||||||
reply_markup=builder.as_markup(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def math_task_true(message: Message, bot: Bot):
|
|
||||||
await message.reply(f"Верно! Добро пожаловать в чат {message.from_user.first_name}")
|
|
||||||
await unmute_user(message.chat.id, message.from_user.id, bot)
|
|
||||||
add_user(
|
|
||||||
message.from_user.id,
|
|
||||||
message.from_user.first_name + " " + message.from_user.last_name,
|
|
||||||
message.from_user.username,
|
|
||||||
)
|
|
||||||
pass
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Welcome",
|
|
||||||
"description": "Мо",
|
|
||||||
"author": "OCAB Team",
|
|
||||||
"version": "1.0"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
from aiogram import F, Router
|
|
||||||
|
|
||||||
from .handlers import check_new_user
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
# Если в чат пришел новый пользователь
|
|
||||||
router.message.register(check_new_user, F.new_chat_members.exists())
|
|
||||||
# Ловин колбеки от кнопок с callback_data=f"check_math_task_true"
|
|
||||||
router.callback_query.register(
|
|
||||||
check_new_user, F.callback_data == "check_math_task_true"
|
|
||||||
)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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}")
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Модуль Admin
|
|
||||||
|
|
||||||
Модуль `admin` предоставляет администраторам и модераторам чата инструменты для управления:
|
|
||||||
|
|
||||||
## Функциональность
|
|
||||||
|
|
||||||
- Удаление сообщений.
|
|
||||||
- Получение ID чата.
|
|
||||||
|
|
||||||
## Команды
|
|
||||||
|
|
||||||
- `/rm` - удалить сообщение, на которое отвечает команда.
|
|
||||||
- `/chatID` - получить ID текущего чата.
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
1. Ответьте на сообщение, которое нужно удалить.
|
|
||||||
2. Отправьте команду `/rm`.
|
|
||||||
|
|
||||||
Чтобы получить ID чата, отправьте команду `/chatID`.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .main import module_init
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
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):
|
|
||||||
reply_message_id = message.reply_to_message.message_id
|
|
||||||
await bot.delete_message(message.chat.id, reply_message_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def error_access(message: Message, bot: Bot):
|
|
||||||
await message.reply("Вы не админ/модератор")
|
|
||||||
|
|
||||||
|
|
||||||
async def get_chat_id(message: Message, bot: Bot):
|
|
||||||
await message.reply(
|
|
||||||
f"ID данного чата: `{message.chat.id}`", parse_mode="MarkdownV2"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def chat_not_in_approve_list(message: Message, bot: Bot):
|
|
||||||
await message.reply(
|
|
||||||
f"Бот недоступен в данном чате, пожалуйста,"
|
|
||||||
f" обратитесь к администратору для добавления чата в список доступных или перейдите в чат "
|
|
||||||
f"{get_default_chat_tag()}"
|
|
||||||
)
|
|
||||||
await get_chat_id(message, bot)
|
|
||||||
|
|
||||||
|
|
||||||
async def mute_user(chat_id: int, user_id: int, time: int, bot: Bot):
|
|
||||||
# *, can_send_messages: bool | None = None, can_send_audios: bool | None = None, can_send_documents: bool | None = None, can_send_photos: bool | None = None, can_send_videos: bool | None = None, can_send_video_notes: bool | None = None, can_send_voice_notes: bool | None = None, can_send_polls: bool | None = None, can_send_other_messages: bool | None = None, can_add_web_page_previews: bool | None = None, can_change_info: bool | None = None, can_invite_users: bool | None = None, can_pin_messages: bool | None = None, can_manage_topics: bool | None = None, **extra_data: Any)
|
|
||||||
|
|
||||||
mutePermissions = {
|
|
||||||
"can_send_messages": False,
|
|
||||||
"can_send_audios": False,
|
|
||||||
"can_send_documents": False,
|
|
||||||
"can_send_photos": False,
|
|
||||||
"can_send_videos": False,
|
|
||||||
"can_send_video_notes": False,
|
|
||||||
"can_send_voice_notes": False,
|
|
||||||
"can_send_polls": False,
|
|
||||||
"can_send_other_messages": False,
|
|
||||||
"can_add_web_page_previews": False,
|
|
||||||
"can_change_info": False,
|
|
||||||
"can_invite_users": False,
|
|
||||||
"can_pin_messages": False,
|
|
||||||
"can_manage_topics": False,
|
|
||||||
}
|
|
||||||
end_time = time + int(time.time())
|
|
||||||
await bot.restrict_chat_member(
|
|
||||||
chat_id, user_id, until_date=end_time, **mutePermissions
|
|
||||||
)
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "standard.admin",
|
|
||||||
"name": "Admin",
|
|
||||||
"description": "Модуль для работы с админкой",
|
|
||||||
"author": "OCAB Team",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"privileged": false,
|
|
||||||
"dependencies": {
|
|
||||||
"required": {
|
|
||||||
"standard.filters": "^1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from ocab_core.modules_system.public_api import register_router
|
|
||||||
|
|
||||||
from .routers import router
|
|
||||||
|
|
||||||
|
|
||||||
async def module_init():
|
|
||||||
register_router(router)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# flake8: noqa
|
|
||||||
from aiogram import F, Router
|
|
||||||
from aiogram.filters import Command
|
|
||||||
|
|
||||||
from ocab_core.modules_system.public_api import get_module, log
|
|
||||||
|
|
||||||
from .handlers import (
|
|
||||||
chat_not_in_approve_list,
|
|
||||||
delete_message,
|
|
||||||
error_access,
|
|
||||||
get_chat_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
(ChatModerOrAdminFilter, ChatNotInApproveFilter) = get_module(
|
|
||||||
"standard.filters", ["ChatModerOrAdminFilter", "ChatNotInApproveFilter"]
|
|
||||||
)
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
# Если сообщение содержит какой либо текст и выполняется фильтр ChatNotInApproveFilter, то вызывается функция chat_not_in_approve_list
|
|
||||||
router.message.register(chat_not_in_approve_list, ChatNotInApproveFilter(), F.text)
|
|
||||||
|
|
||||||
router.message.register(get_chat_id, ChatModerOrAdminFilter(), Command("chatID"))
|
|
||||||
|
|
||||||
router.message.register(delete_message, ChatModerOrAdminFilter(), Command("rm"))
|
|
||||||
router.message.register(error_access, Command("rm"))
|
|
||||||
router.message.register(error_access, Command("chatID"))
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Модуль 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")
|
|
||||||
```
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .main import module_init, register_command
|
|
||||||
@@ -1,14 +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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict
|
|
||||||
|
|
||||||
from aiogram import BaseMiddleware
|
|
||||||
from aiogram.types import BotCommand, TelegramObject
|
|
||||||
|
|
||||||
from ocab_core.modules_system.public_api import (
|
|
||||||
get_module,
|
|
||||||
register_outer_message_middleware,
|
|
||||||
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: "IDbApi" = get_module(
|
|
||||||
"standard.database",
|
|
||||||
"db_api",
|
|
||||||
)
|
|
||||||
|
|
||||||
Roles: "IRoles" = get_module("standard.roles", "Roles")
|
|
||||||
|
|
||||||
|
|
||||||
def register_command(command, description, role="USER"):
|
|
||||||
if role not in commands:
|
|
||||||
commands[role] = dict()
|
|
||||||
commands[role][command] = {
|
|
||||||
"description": description,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class OuterMiddleware(BaseMiddleware):
|
|
||||||
async def __call__(
|
|
||||||
self,
|
|
||||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
|
||||||
event: TelegramObject,
|
|
||||||
data: Dict[str, Any],
|
|
||||||
):
|
|
||||||
|
|
||||||
# if not isinstance(event, Message):
|
|
||||||
# return await handler(event, data)
|
|
||||||
#
|
|
||||||
# user = db_api.get_user(event.from_user.id)
|
|
||||||
#
|
|
||||||
# if user is None:
|
|
||||||
# return
|
|
||||||
#
|
|
||||||
# roles = Roles()
|
|
||||||
# role_name = await roles.get_role_name(role_id=user.user_role)
|
|
||||||
#
|
|
||||||
# if role_name not in commands:
|
|
||||||
# return await handler(event, data)
|
|
||||||
|
|
||||||
# bot_commands = []
|
|
||||||
|
|
||||||
# for role_command in commands[role_name]:
|
|
||||||
# bot_commands.append(
|
|
||||||
# BotCommand(
|
|
||||||
# command=role_command,
|
|
||||||
# description=commands[role_name][role_command]["description"],
|
|
||||||
# )
|
|
||||||
# )
|
|
||||||
|
|
||||||
# await event.bot.set_my_commands(
|
|
||||||
# bot_commands,
|
|
||||||
# BotCommandScopeChatMember(
|
|
||||||
# chat_id=event.chat.id,
|
|
||||||
# user_id=event.from_user.id,
|
|
||||||
# ),
|
|
||||||
# )
|
|
||||||
|
|
||||||
return await handler(event, data)
|
|
||||||
|
|
||||||
|
|
||||||
async def module_init():
|
|
||||||
register_outer_message_middleware(OuterMiddleware())
|
|
||||||
|
|
||||||
|
|
||||||
async def set_user_commands():
|
|
||||||
bot_commands = []
|
|
||||||
if "USER" in commands:
|
|
||||||
user_commands = commands["USER"]
|
|
||||||
for command in user_commands:
|
|
||||||
bot_commands.append(
|
|
||||||
BotCommand(
|
|
||||||
command=command,
|
|
||||||
description=user_commands[command]["description"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
await set_my_commands(
|
|
||||||
bot_commands,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def module_late_init():
|
|
||||||
await set_user_commands()
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Модуль 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")
|
|
||||||
```
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from .config import IConfig, config
|
|
||||||
from .main import module_late_init
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# flake8: noqa
|
|
||||||
|
|
||||||
from .config_manager import ConfigManager
|
|
||||||
|
|
||||||
IConfig = ConfigManager
|
|
||||||
|
|
||||||
config: ConfigManager = ConfigManager(config_path="config.yaml")
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "standard.config",
|
|
||||||
"name": "Config YAML",
|
|
||||||
"description": "Модуль для работы с конфигурационным файлом бота (YAML)",
|
|
||||||
"author": "OCAB Team",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"privileged": true,
|
|
||||||
"dependencies": {
|
|
||||||
"optional": {
|
|
||||||
"standard.miniapp": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pythonDependencies": {
|
|
||||||
"optional": {
|
|
||||||
"flet": "^0.23.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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,
|
|
||||||
)
|
|
||||||
pass
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log(str(e))
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def module_late_init():
|
|
||||||
register_settings_page()
|
|
||||||
|
|
||||||
pass
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user