0
0
mirror of https://gitflic.ru/project/maks1ms/ocab.git synced 2025-11-28 10:21:55 +03:00

56 Commits

Author SHA1 Message Date
2becd33774 wip 2024-08-01 11:16:08 +03:00
9fa23f18b9 обновил документацию 2024-07-31 20:01:32 +03:00
63b321a500 улучшена документация 2024-07-31 19:24:27 +03:00
ffa5af740e добавлен workspace 2024-07-31 17:03:50 +03:00
3130e820c3 исправлено сохранение настроек 2024-07-31 15:20:13 +03:00
d5f6f1bb4f подготовка к публикации 2024-07-28 18:48:25 +03:00
bfa1d13931 wip 2024-07-28 15:24:28 +03:00
5b02c2ce6a добавил типизацию 2024-07-27 22:23:42 +03:00
e48e83bf2c wip 2024-07-24 16:28:42 +03:00
6aab1ee244 wip 2024-07-22 00:44:27 +03:00
2177c712a3 фикс логгирования в hypercorn 2024-07-21 21:27:57 +03:00
34c365178b wip 2024-07-21 20:01:50 +03:00
d52864a231 wip 2024-07-20 12:01:00 +03:00
e8b5f79d99 wip 2024-07-18 11:32:09 +03:00
abf8f8047c Добавлен .mailmap 2024-07-17 19:37:48 +03:00
eecc59ca94 Merged with private/new-module-system 2024-07-14 17:07:46 +03:00
370b4fc648 Merged with private/new-module-system 2024-07-10 19:30:23 +03:00
2a2b9e15e8 Merged with chore/code-quality-tools 2024-07-08 14:20:21 +03:00
ef10f05a73 Добавлены nosec для прохождения bandit 2024-07-08 00:55:33 +03:00
ef0dda07f7 Добавлен bandit в pre-commit-hook 2024-07-08 00:49:04 +03:00
e80a01157f Автоматический рефакторинг и игнорирование flake8
Выполнен автоматический рефакторинг. Для тех файлов,
которые не прошли flake8 - был добавлен `noqa`, чтобы
в будущем исправить эти проблемы
2024-07-08 00:38:01 +03:00
4edeef4003 Добавлен pre-commit 2024-07-08 00:21:50 +03:00
31142dfb1c Добавлены инструменты для повышения качества кода 2024-07-07 23:59:33 +03:00
837613e072 Merged with chore/refactor-core 2024-07-07 21:25:10 +03:00
armatik
9d89aee578 Открытое бета тестирование + фиксы первого дня 2024-05-10 18:18:08 +03:00
armatik
370ac7d02b Исправление конфига 2024-05-01 01:50:29 +03:00
armatik
85a357e37b Множественные мелкие исправления 2024-05-01 01:50:18 +03:00
armatik
661209055d Тестирование YandexGPT API 2024-05-01 01:49:20 +03:00
armatik
53ed19d25c Poetry fix 2024-04-30 23:58:30 +03:00
Armatik
c6e60b7810 Изменение названия db_api в main.py 2024-04-25 15:57:37 +03:00
Armatik
2cbe898a57 Merge remote-tracking branch 'origin/OCAB-V2' into OCAB-V2
# Conflicts:
#	src/core/main.py
2024-04-25 15:57:06 +03:00
Armatik
ca9834206b Начало разработки модуля модерации 2024-04-25 15:55:50 +03:00
Armatik
fb6da3d627 Изменение наименования db_api 2024-04-25 15:55:23 +03:00
Armatik
b7fdd75458 Доработка реквестов и исправление ошибок 2024-04-25 15:54:44 +03:00
Armatik
28e157a9b6 Более понятное наименование 2024-04-25 15:54:27 +03:00
Armatik
192b2ed51c Добавлено получение id роли пользователя 2024-04-25 15:42:17 +03:00
ilyazheprog
4d2f61b1c2 Добавил /chatID
Проверка админ прав используя телеграм
2024-02-25 03:59:26 +07:00
fiersik
4d6ee64731 Обновил pyproject 2024-02-24 23:49:50 +04:00
fiersik
0dc216d9f4 Обновил how-to install deps 2024-02-24 21:13:51 +04:00
ilyazheprog
88f40c7f82 Админ модуль: удаление сообщений
Модуль фильтров
Добавлены роутеры
2024-02-12 14:55:14 +07:00
ilyazheprog
d71fef0fed обновил стек: Python 3.11.6 или выше 2024-02-12 11:48:55 +07:00
qualimock
68406fee53 Fix E302 2024-02-05 15:51:50 +03:00
Armatik
cfaffd337d Мелки исправления форматирование и описание модуля welcome 2024-02-05 15:43:26 +03:00
armatik
6f1efa6b7b Реализован модуль YandexGPT первой версии. Реализованы методы: генерация ответа с помощью YandexGPT и YandexGPT-lite, перевод текста с помощью YandexGPT, проверка орфографии и пунктуации с помощью YandexGPT, краткий пересказ истории чата с помощью Yandex Text Summarization Model. 2024-01-14 00:57:33 +03:00
Armatik
dee4f5bc79 Начало разработки модуля YandexGPT 2024-01-02 22:39:00 +03:00
Armatik
649e780109 Добавлены новые методы и изменены старые в API модуля DataBase. 2024-01-02 21:26:09 +03:00
Armatik
7918620b99 Изменение в моделях баз данных. Добавлен README.md к модулю DataBase с информацией о базе данных. 2024-01-02 14:59:10 +03:00
ilyazheprog
1e7c70078b Исправлены импорты внутри модулей, добавлены: модуль для исключений, скрипт инициализации путей, переделано подключение к бд 2023-12-10 23:04:17 +07:00
ilyazheprog
88db66c360 добавлены модули config, role, исправлена бд 2023-12-07 18:02:04 +07:00
ilyazheprog
bc5de691a7 написана точка входа бота, конфиг, добавлена зависимость PyYAML 2023-11-30 20:23:37 +07:00
ilyazheprog
0d614cee69 модуль Database приведён к правильному виду 2023-11-29 20:09:08 +07:00
ilyazheprog
b33db9203a Уточнил версии в README.md "Технологический стек" 2023-11-27 15:18:20 +07:00
armatik
d7bafdf513 Базовая разметка README.md 2023-11-27 00:27:41 +03:00
ilyazheprog
8378625694 Написал интерфейс взаимодействия с БД и покрыл тестами 2023-11-26 20:23:39 +07:00
ilyazheprog
9dde0530d0 Внедрил poetry, установил в зависимости: aiogram3, peewee, добавил гайд на установку poetry 2023-11-26 12:24:47 +07:00
armatik
9e64cbfc4c Удалены старые директории 2023-11-25 19:46:09 +03:00
171 changed files with 11779 additions and 1219 deletions

1
.bandit Normal file
View File

@@ -0,0 +1 @@
[bandit]

6
.flake8 Normal file
View File

@@ -0,0 +1,6 @@
[flake8]
per-file-ignores =
__init__.py:F401
max-line-length = 88
count = true
extend-ignore = E203,E701

4
.gitignore vendored
View File

@@ -2,5 +2,9 @@
.vscode
.env
env
.venv
venv
__pycache__
OCAB.db
config.yaml
dist

7
.mailmap Normal file
View File

@@ -0,0 +1,7 @@
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>

30
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,30 @@
# 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

View File

@@ -1 +1,44 @@
# OpenChatAiBot
# OCAB - Open Chat Ai Bot
## Что такое OCAB?
OCAB - это платформа для разработки модульных Telegram-ботов, которая призвана упростить взаимодействие с чатами.
OCAB предоставляет возможность расширять функциональность бота с помощью интеграции различных модулей.
Код платформы и набор стандартных модулей находятся в этом монорепозитории.
## Структура монорепозитория
Монорепозиторий OCAB включает в себя:
* **Ядро OCAB (src/ocab_core):** Содержит основные компоненты платформы, такие как система управления модулями,
логирование и утилиты.
* **Модули OCAB (src/ocab_modules):** Содержит стандартные и дополнительные модули, которые расширяют
функциональность ботов OCAB.
* **Пример бота (src/gnomik):** Пример реализации бота на платформе OCAB.
## Модули
Модули OCAB - это независимые компоненты, которые добавляют функциональность к боту.
### Структура модуля
Структура модуля представлена [здесь](docs/MODULES-SPEC.md).
### Стандартные модули
Стандартные модули предоставляют базовые функции для работы бота:
* [admin](src/ocab_modules/ocab_modules/standard/admin/README.md) - модуль для модерирования чата.
* [roles](src/ocab_modules/ocab_modules/standard/roles/README.md) - модуль ролей пользователей.
* [config](src/ocab_modules/ocab_modules/standard/config/README.md) - модуль управления конфигурацией бота.
* [database](src/ocab_modules/ocab_modules/standard/database/README.md) - модуль для работы с базой данных.
* [fsm_database_storage](src/ocab_modules/ocab_modules/standard/fsm_database_storage/README.md) - модуль для хранения состояний FSM в базе данных.
* [filters](src/ocab_modules/ocab_modules/standard/filters/README.md) - модуль, предоставляющий фильтры для aiogram.
* [message_processing](src/ocab_modules/ocab_modules/standard/message_processing/README.md) - модуль обработки входящих сообщений.
* [miniapp](src/ocab_modules/ocab_modules/standard/miniapp/README.md) - модуль для реализации веб-интерфейса бота.
* [command_helper](src/ocab_modules/ocab_modules/standard/command_helper/README.md) - модуль для упрощения регистрации команд бота.
* [info](src/ocab_modules/ocab_modules/standard/info/README.md) - модуль предоставления информации о пользователях и чатах.
### Дополнительные официальные модули
Дополнительные официальные модули разработаны командой OCAB и предоставляют расширенные возможности для бота:
* [yandexgpt](src/ocab_modules/ocab_modules/external/yandexgpt/README.md) - модуль для интеграции с нейросетью YandexGPT.
* [create_report_apps](src/ocab_modules/ocab_modules/external/create_report_apps/README.md) - модуль для создания отчетов об ошибках.

39
docs/DEV.md Normal file
View File

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

107
docs/MODULES-SPEC.md Normal file
View File

@@ -0,0 +1,107 @@
# Спецификация модулей
> **Внимание!**
>
> Данная спецификация еще не закончена и активно разрабатывается.
> Могут быть значительные изменения (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`. Она позволяет получить модуль или предоставляемые им объекты по его
идентификатору.

25
ocab.code-workspace Normal file
View File

@@ -0,0 +1,25 @@
{
"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 Normal file
View File

@@ -0,0 +1,475 @@
# 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"

2
poetry.toml Normal file
View File

@@ -0,0 +1,2 @@
[virtualenvs]
in-project = true

49
pyproject.toml Normal file
View File

@@ -0,0 +1,49 @@
[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"

View File

@@ -1,3 +0,0 @@
aiogram==3.1.1
openai==0.27.8
PyYAML==6.0.1

21
scripts/init.py Normal file
View File

@@ -0,0 +1,21 @@
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()

115
scripts/module.py Normal file
View File

@@ -0,0 +1,115 @@
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()

9
scripts/test.py Normal file
View File

@@ -0,0 +1,9 @@
import subprocess # nosec
def main():
subprocess.run(["python", "-u", "-m", "unittest", "discover"]) # nosec
if __name__ == "__main__":
main()

View File

@@ -1,198 +0,0 @@
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

View File

@@ -1,8 +0,0 @@
# Получение сообщений в чате, и запись их в базу данных
from aiogram import types
from src.TelegramBot.main import dp
from main import cursor, config
#импортировать функцию для обработки сообщений из OpenAI

View File

@@ -1,566 +0,0 @@
# Импорт библиотек
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)

View File

@@ -1,43 +0,0 @@
[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" Пока не используется

View File

@@ -1,23 +0,0 @@
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:

View File

@@ -1,5 +0,0 @@
from aiogram.filters import BaseFilter
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
# You filters

View File

@@ -1,4 +0,0 @@
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
# Your handlers

View File

@@ -1,3 +0,0 @@
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
# Your keyboards

View File

@@ -1,3 +0,0 @@
from aiogram.fsm.context import FSMContext
# Your funcs

View File

@@ -1,5 +0,0 @@
from aiogram.fsm.state import State, StatesGroup
class MyState(StatesGroup):
state1 = State()

View File

@@ -1 +0,0 @@
# Here text of buttons

View File

@@ -1,14 +0,0 @@
from aiogram import Dispatcher
def registry_middlewares(dp: Dispatcher):
pass
def registry_handlers(dp: Dispatcher):
pass
def registry(dp: Dispatcher):
registry_middlewares(dp)
registry_handlers(dp)

View File

@@ -1,62 +0,0 @@
import openai
import configparser
from dataclasses import dataclass
@dataclass
class App:
telegram_token: str
openai_key: str
bot_trigger_front: list[str]
bot_trigger_all: list[str]
@dataclass
class Database:
path: str
message_limit: int
@dataclass
class Config:
app: App
database: Database
def _clean_bot_trigger(bot_trigger_front, bot_trigger_all):
"""удаление лишних элементов массивов"""
bot_trigger_front.remove('')
bot_trigger_all.remove('')
def get_settings():
# Импорт переменных из файла .ini
config = configparser.ConfigParser()
config.read('core/config.ini')
bot_trigger_front = (config['Telegram']['bot_trigger_front']).split('|')
bot_trigger_all = (config['Telegram']['bot_trigger_all']).split('|')
_clean_bot_trigger(bot_trigger_front, bot_trigger_all)
return Config(
app=App(
telegram_token=config['Telegram']['token'],
openai_key=config['Openai']['api_key'],
bot_trigger_front = bot_trigger_front,
bot_trigger_all = bot_trigger_all,
),
database=Database(
path="core/database/data.db",
message_limit=int(config['DataBase']['message_limit']),
),
)
def _openai_init(settings: Config):
openai.api_key = settings.app.openai_key
settings = get_settings()
_openai_init(settings)

View File

@@ -1,28 +0,0 @@
from asyncio import run
from aiogram import Bot, Dispatcher
from core.utils.registry import registry
from core.utils.settings import settings
async def start(bot: Bot, dp: Dispatcher):
try:
await dp.start_polling(bot)
finally:
await bot.session.close()
# close db connection here
def main():
bot = Bot(token=settings.app.telegram_token)
dp = Dispatcher()
registry(dp)
run(start(bot, dp))
if __name__ == "__main__":
main()

View File

@@ -1,43 +0,0 @@
[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" Пока не используется

View File

@@ -1,23 +0,0 @@
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:

View File

@@ -1 +0,0 @@

23
src/gnomik/Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
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"]

View File

@@ -0,0 +1,14 @@
**/Dockerfile
**/*.dockerignore
**/docker-compose.yml
**/.git
**/.gitignore
**/.venv
**/.mypy_cache
**/__pycache__/
src/gnomik/config.yaml
src/gnomik/database/*

55
src/gnomik/README.md Normal file
View File

@@ -0,0 +1,55 @@
# Gnomик
![Логотип](./docs/gnomik.jpg)
Чат-бот помощник в [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: описать функционал
-->
## Запуск
### Docker
1. Соберите Docker-образ:
```bash
docker build -t gnomik .
```
2. Запустите контейнер:
```bash
docker run -p 9000:9000 -v ./config.yaml:/app/config.yaml -v ./database:/app/database gnomik
```
Замените `./config.yaml` и `./database` на пути к вашим локальным файлам конфигурации и паки для базы данных.
### Вручную
1. Активируйте виртуальное окружение Gnomика:
```bash
poetry shell
```
2. Запустите бота:
```bash
python -m gnomik
```
## Конфигурация
Конфигурация бота находится в файле `config.yaml`.
## Модули
Список загружаемых модулей указан в файле `__main__.py`.

View File

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

View File

@@ -0,0 +1,12 @@
version: '3'
services:
app:
build:
context: ../..
dockerfile: src/gnomik/Dockerfile
ports:
- 9000:9000
volumes:
- ./config.yaml:/app/config.yaml
- ./database:/app/database

BIN
src/gnomik/docs/gnomik.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -0,0 +1,29 @@
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 Normal file

File diff suppressed because it is too large Load Diff

2
src/gnomik/poetry.toml Normal file
View File

@@ -0,0 +1,2 @@
[virtualenvs]
in-project = true

15
src/gnomik/pyproject.toml Normal file
View File

@@ -0,0 +1,15 @@
[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"

View File

@@ -1,21 +0,0 @@
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']

View File

@@ -1,131 +0,0 @@
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()

View File

@@ -1,33 +0,0 @@
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

23
src/ocab_core/README.md Normal file
View File

@@ -0,0 +1,23 @@
# OCAB Core
Это ядро OCAB, содержащее базовые компоненты:
- Система управления модулями.
- Логирование.
- Утилиты.
## Система управления модулями
Система управления модулями отвечает за:
- Загрузку модулей.
- Проверку зависимостей.
- Предоставление API для взаимодействия между модулями.
## Логирование
Модуль логирования предоставляет функции для записи логов в консоль.
## Утилиты
Модуль утилит содержит вспомогательные функции, например, для форматирования текста.

View File

@@ -0,0 +1 @@
from .main import OCAB

View File

@@ -0,0 +1,38 @@
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

View File

@@ -0,0 +1,46 @@
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

View File

@@ -0,0 +1,118 @@
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(config.get("miniapp::prefix"), 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,
)

View File

@@ -0,0 +1 @@
from .modules_manager import ModulesManager

View File

@@ -0,0 +1,2 @@
from .fs_loader import FSLoader
from .unsafe_fs_loader import UnsafeFSLoader

View File

@@ -0,0 +1,43 @@
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

View File

@@ -0,0 +1,91 @@
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

View File

@@ -0,0 +1 @@
from .FSLoader import FSLoader

View File

@@ -0,0 +1,61 @@
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

View File

@@ -0,0 +1 @@
from .UnsafeFSLoader import UnsafeFSLoader

View File

@@ -0,0 +1,169 @@
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"]

View File

@@ -0,0 +1,12 @@
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

View File

@@ -0,0 +1,131 @@
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)

View File

@@ -0,0 +1,12 @@
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>"

View File

@@ -0,0 +1,144 @@
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

View File

@@ -0,0 +1,225 @@
#############################################################################
#
# 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

View File

@@ -0,0 +1,25 @@
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 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
[virtualenvs]
in-project = true

View File

@@ -0,0 +1,26 @@
[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"

View File

@@ -0,0 +1,12 @@
# OCAB Modules
OCAB Modules содержит набор модулей для платформы Open Chat AI Bot (OCAB).
## Описание
OCAB - это платформа для создания чат-ботов Telegram. Модули - это расширения, которые добавляют функциональность ботам OCAB.
## Типы модулей
* **Стандартные модули (standard.*):** Предоставляют основные функции, такие как управление пользователями, ролями и настройками.
* **Дополнительные официальные модули (external.*):** Разработаны командой OCAB и предоставляют расширенные возможности, такие как интеграция с нейросетями, внешними сервисами и API.

View File

@@ -0,0 +1 @@
from .lib import module_loader

View File

@@ -0,0 +1 @@
from . import yandexgpt

View File

@@ -0,0 +1,19 @@
# Модуль Create Report Apps
Модуль `create_report_apps` предназначен для помощи пользователям в создании отчетов об ошибках в приложениях.
## Функциональность
- Задает пользователю ряд вопросов, необходимых для составления отчета.
- Собирает информацию о системе пользователя.
- Формирует отчет в текстовом формате.
## Команды
- `/create_report_apps` - запустить процесс создания отчета.
## Использование
1. Отправьте команду `/create_report_apps` боту в личных сообщениях или в групповом чате.
2. Ответьте на вопросы бота.
3. Бот сформирует отчет и отправит его вам.

View File

@@ -0,0 +1 @@
from .main import module_init

View File

@@ -0,0 +1,136 @@
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()

View File

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

View File

@@ -0,0 +1,115 @@
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", "Написать репорт о приложении")

View File

@@ -0,0 +1,59 @@
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

View File

@@ -0,0 +1,22 @@
# Модуль YandexGPT
Модуль `yandexgpt` интегрирует в бота OCAB нейросеть YandexGPT.
## Функциональность
- Позволяет боту отвечать на сообщения пользователей, используя YandexGPT.
- Строит линию контекста для нейросети, используя историю сообщений.
## Конфигурация
- `yandexgpt::token` - API-ключ для доступа к YandexGPT.
- `yandexgpt::catalogid` - идентификатор каталога YandexGPT.
- `yandexgpt::prompt` - системная подсказка для YandexGPT.
- `yandexgpt::startword` - слова, с которых должно начинаться сообщение, чтобы бот ответил.
- `yandexgpt::inword` - слова, которые должны быть в сообщении, чтобы бот ответил.
## Использование
1. Настройте конфигурационные параметры модуля.
2. Отправьте боту сообщение, которое соответствует условиям, указанным в параметрах `startword` и `inword`.
3. Бот ответит на сообщение, используя YandexGPT.

View File

@@ -0,0 +1,2 @@
from .handlers import answer_to_message
from .main import module_init

View File

@@ -0,0 +1,47 @@
# 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")

View File

@@ -0,0 +1,21 @@
{
"id": "external.yandexgpt",
"name": "Yandex GPT",
"description": "Модуль для работы с Yandex GPT",
"author": "OCAB Team",
"version": "1.0.0",
"privileged": false,
"dependencies": {
"required": {
"standard.config": "^1.0.0",
"standard.database": "^1.0.0"
}
},
"pythonDependencies": {
"required": {
"aiohttp": "*",
"requests": "*",
"json": "*"
}
}
}

View File

@@ -0,0 +1,50 @@
from typing import TYPE_CHECKING
from ocab_core.modules_system.public_api import get_module
if TYPE_CHECKING:
from ocab_modules.standard.config import IConfig
config: "IConfig" = get_module("standard.config", "config")
def module_init():
config.register(
"yandexgpt::token",
"password",
required=True,
)
config.register(
"yandexgpt::token_for_request",
"int",
default_value=8000,
)
config.register(
"yandexgpt::token_for_answer",
"int",
default_value=2000,
)
config.register(
"yandexgpt::catalogid",
"password",
required=True,
)
config.register(
"yandexgpt::prompt",
"string",
default_value="Ты чат-бот ...",
)
config.register(
"yandexgpt::startword",
"string",
default_value="Бот| Бот, | бот | бот,",
)
config.register(
"yandexgpt::inword",
"string",
default_value="помогите | не работает",
)

View File

@@ -0,0 +1,10 @@
# 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("гномик")
)

View File

@@ -0,0 +1,312 @@
# 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"

View File

@@ -0,0 +1 @@
from .moderation import ban_user, unmute_user

View File

@@ -0,0 +1,6 @@
{
"name": "Moderation",
"description": "Moderation commands for OCAB",
"author": "OCAB Team",
"version": "1.0"
}

View File

@@ -0,0 +1,82 @@
# 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)

View File

@@ -0,0 +1,84 @@
# 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

View File

@@ -0,0 +1,6 @@
{
"name": "Welcome",
"description": "Мо",
"author": "OCAB Team",
"version": "1.0"
}

View File

@@ -0,0 +1,12 @@
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"
)

Some files were not shown because too many files have changed in this diff Show More