mirror of
https://gitflic.ru/project/alt-gnome/karkas.git
synced 2025-04-08 18:43:48 +03:00
Compare commits
103 Commits
Author | SHA1 | Date | |
---|---|---|---|
4d25d2e83c | |||
7c25c7b4a9 | |||
e0a8bea938 | |||
f97fcf4c34 | |||
3edee6e930 | |||
3434230446 | |||
bd3a897557 | |||
3018bd217f | |||
7b81191ad4 | |||
9669118bf6 | |||
8e26f43587 | |||
56b5a73f8d | |||
c9afdf46ca | |||
b4eac59f80 | |||
6c9e13b5db | |||
b1d95a1496 | |||
750b6be3a5 | |||
f64800949c | |||
fe9bbebd79 | |||
9ac88652e0 | |||
0881fe02d5 | |||
7724a60f8c | |||
|
a6c0433569 | ||
5950fa3bb8 | |||
8f2adbfbc4 | |||
8baabcc413 | |||
fab0b84a61 | |||
64a4066825 | |||
8ec93c89b7 | |||
|
6d421ee9b5 | ||
|
5f5e851ecd | ||
|
04a4ab4868 | ||
aab5fb4e39 | |||
5a76e94c41 | |||
ea7e12c5ed | |||
7ebe631f9f | |||
f5e15868f3 | |||
9c9c93edf5 | |||
5513481330 | |||
8512b3300b | |||
a3f0298288 | |||
fa89265197 | |||
7c196371c0 | |||
a28e0b308f | |||
9fb56ce1e0 | |||
f5f662d6de | |||
c4f42bcf25 | |||
a7f1631869 | |||
8af22b76b2 | |||
707ce28182 | |||
827e6f6b44 | |||
dc1abeec9f | |||
1c1a8e1038 | |||
53065a3871 | |||
a25a97e8f7 | |||
d987938ef9 | |||
21ae060c81 | |||
b349a555eb | |||
|
3c0c8630eb | ||
|
1fbe2b0c18 | ||
|
2f634a4eef | ||
|
3c7dffc06d | ||
|
5b3963e87c | ||
|
9fa5776fda | ||
df1fed10c2 | |||
79298e5441 | |||
3514234526 | |||
913d84fb81 | |||
ac72ec7fa4 | |||
3f46a83de3 | |||
27a37b2f67 | |||
2c06017cc3 | |||
c01dbcbe6b | |||
9ca32cfa28 | |||
f6f0f8c02b | |||
58281f2580 | |||
b7cba315d7 | |||
afbd277428 | |||
15cb6afb34 | |||
81ddb8509f | |||
|
984e4cf4e1 | ||
|
b185acd871 | ||
|
39500b77c2 | ||
|
3076e1af13 | ||
|
a9f6800518 | ||
|
3ff4987bad | ||
|
83bfcdddf7 | ||
73c1eb12e9 | |||
5732b1bcc3 | |||
e3443f835a | |||
c746946d24 | |||
01912850e2 | |||
067fa52719 | |||
e2f5e8daba | |||
eecc59ca94 | |||
370b4fc648 | |||
2a2b9e15e8 | |||
ef10f05a73 | |||
ef0dda07f7 | |||
e80a01157f | |||
4edeef4003 | |||
31142dfb1c | |||
837613e072 |
8
.flake8
Normal file
8
.flake8
Normal file
@ -0,0 +1,8 @@
|
||||
[flake8]
|
||||
per-file-ignores =
|
||||
__init__.py:F401
|
||||
max-line-length = 88
|
||||
count = true
|
||||
extend-ignore = E203,E701
|
||||
|
||||
extend-select = TC010,TC200
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -5,6 +5,7 @@ env
|
||||
.venv
|
||||
venv
|
||||
__pycache__
|
||||
OCAB.db
|
||||
src/paths.json
|
||||
src/core/config.yaml
|
||||
Karkas.db
|
||||
config.yaml
|
||||
dist
|
||||
*.py[cod]
|
||||
|
7
.mailmap
Normal file
7
.mailmap
Normal 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>
|
81
.pre-commit-config.yaml
Normal file
81
.pre-commit-config.yaml
Normal file
@ -0,0 +1,81 @@
|
||||
# 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/netromdk/vermin
|
||||
rev: v1.6.0
|
||||
hooks:
|
||||
- id: vermin
|
||||
args: ['-t=3.10-', '--violations']
|
||||
- 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
|
||||
additional_dependencies:
|
||||
- flake8-type-checking
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.9 # sync:bandit:poetry.lock
|
||||
hooks:
|
||||
- id: bandit
|
||||
- repo: https://github.com/python-poetry/poetry
|
||||
rev: 1.8.3
|
||||
hooks:
|
||||
- name: Poetry Lock (root)
|
||||
id: poetry-lock
|
||||
args: ["--no-update"]
|
||||
- name: Poetry Check (root)
|
||||
id: poetry-check
|
||||
- name: Poetry Lock (gnomik)
|
||||
id: poetry-lock
|
||||
args: ["-C", "./src/gnomik", "--no-update"]
|
||||
- name: Poetry Check (gnomik)
|
||||
id: poetry-check
|
||||
args: ["-C", "./src/gnomik"]
|
||||
- name: Poetry Lock (altlinux)
|
||||
id: poetry-lock
|
||||
args: ["-C", "./src/altlinux", "--no-update"]
|
||||
- name: Poetry Check (altlinux)
|
||||
id: poetry-check
|
||||
args: ["-C", "./src/altlinux"]
|
||||
- name: Poetry Lock (karkas_core)
|
||||
id: poetry-lock
|
||||
args: ["-C", "./src/karkas_core", "--no-update"]
|
||||
- name: Poetry Check (karkas_core)
|
||||
id: poetry-check
|
||||
args: ["-C", "./src/karkas_core"]
|
||||
- name: Poetry Lock (karkas_blocks)
|
||||
id: poetry-lock
|
||||
args: ["-C", "./src/karkas_blocks", "--no-update"]
|
||||
- name: Poetry Check (karkas_blocks)
|
||||
id: poetry-check
|
||||
args: ["-C", "./src/karkas_blocks"]
|
||||
- name: Poetry Lock (karkas_piccolo)
|
||||
id: poetry-lock
|
||||
args: ["-C", "./src/karkas_piccolo", "--no-update"]
|
||||
- name: Poetry Check (karkas_piccolo)
|
||||
id: poetry-check
|
||||
args: ["-C", "./src/karkas_piccolo"]
|
||||
- repo: https://github.com/compilerla/conventional-pre-commit
|
||||
rev: v3.4.0
|
||||
hooks:
|
||||
- id: conventional-pre-commit
|
||||
stages: [commit-msg]
|
||||
args: []
|
8
AUTHORS
Normal file
8
AUTHORS
Normal file
@ -0,0 +1,8 @@
|
||||
Руководитель проекта:
|
||||
- Семен Фомченков (@Armatik), e-mail: armatik@alt-gnome.ru
|
||||
|
||||
Ведущие разработчики:
|
||||
- Максим Слипенко (@Maks1m_S), e-mail: maxim@slipenko.com
|
||||
|
||||
Участники проекта:
|
||||
- Илья Женецкий (@ilyazheprog)
|
8
AUTHORS_EN
Normal file
8
AUTHORS_EN
Normal file
@ -0,0 +1,8 @@
|
||||
Project manager:
|
||||
- Semen Fomchenkov (@Armatik), e-mail: armatik@alt-gnome.ru
|
||||
|
||||
Leading developers:
|
||||
- Maxim Slipenko (@Maks1m_S), e-mail: maxim@slipenko.com
|
||||
|
||||
Project participants:
|
||||
- Ilya Zhenetsky (@ilyazheprog)
|
71
README.md
71
README.md
@ -1,58 +1,45 @@
|
||||
# OpenChatAiBot V2
|
||||
# Каркас
|
||||
|
||||
## Что такое OCAB?
|
||||
## Что такое «Каркас»?
|
||||
|
||||
OCAB - это бот для Telegram, который призван помочь во взаимодействии с чатом.
|
||||
Бот поддерживает интеграцию модулей для расширения функционала.
|
||||
Фактически бот является платформой для запуска созданных для него модулей.
|
||||
Модули могут взаимодействовать друг с другом или быть полностью независимыми.
|
||||
Каркас — это платформа для разработки блочных Telegram-ботов, которая призвана упростить взаимодействие с чатами. «Каркас» предоставляет возможность расширять функциональность бота с помощью интеграции различных блоков. Код платформы и набор стандартных блоков находятся в этом монорепозитории.
|
||||
|
||||
## Что такое модуль?
|
||||
## Структура монорепозитория
|
||||
|
||||
Модуль - это директория, которая содержит в себе код модуля и его конфигурацию.
|
||||
Монорепозиторий Karkas включает в себя:
|
||||
|
||||
### Структура модуля
|
||||
- **Ядро Karkas (`src/karkas_core`):** Основные компоненты платформы, такие как система управления блоками, логирование и утилиты.
|
||||
- **Блоки Karkas (`src/karkas_blocks`):** Содержит стандартные и дополнительные блоки, которые расширяют функциональность ботов, созданных на платформе «Каркас».
|
||||
- **Бот Gnomик (`src/gnomik`):** Пример реализации бота, созданного на основе платформы «Каркас».
|
||||
|
||||
*Будет дополнено после закрытия [issue #17](https://gitflic.ru/project/armatik/ocab/issue/17).*
|
||||
## Блоки
|
||||
|
||||
## Стандартные модули
|
||||
Блоки Karkas — это независимые компоненты, которые добавляют функциональность бота.
|
||||
|
||||
В стандартный состав бота входят следующие модули:
|
||||
### Структура блока
|
||||
|
||||
* `admin` - модуль для модерирования чата. Позволяет удалять сообщения, банить пользователей и т.д.
|
||||
* `reputation` - модуль репутации пользователей. Позволяет оценивать ответы пользователей и накапливать репутацию.
|
||||
* `welcome` - модуль приветствия новых пользователей. Позволяет приветствовать новых пользователей в чате, а также
|
||||
проверять пользователя капчей для предотвращения спама.
|
||||
* `roles` - модуль ролей. Позволяет назначать пользователям роли и ограничивать доступ к командам бота по ролям.
|
||||
Является важной частью системы прав доступа и модуля `admin`.
|
||||
Структура блока представлена [здесь](docs/BLOCKS-SPEC.md).
|
||||
|
||||
## Дополнительные официальные модули
|
||||
### Стандартные блоки
|
||||
|
||||
* `gpt` - модуль для генерации ответов на основе нейросети GPT-3.5. Позволяет боту отвечать на сообщения
|
||||
пользователей, используя нейросеть. Ключевой особенностью является построение линии контекста для нейросети,
|
||||
которая позволяет боту отвечать на вопросы, используя контекст предыдущих сообщений. Для этого используется
|
||||
модуль база данных хранящий историю сообщений.
|
||||
* `bugzilla` - модуль для интеграции с BugZilla. Позволяет получать уведомления о новых багах в BugZilla, отслеживать их
|
||||
статус, формировать стандартизированные сообщения для корректного описания багов. В будущем планируется интеграция с
|
||||
API BugZilla для возможности создания багов из чата.
|
||||
* `alt_packages` - модуль для интеграции с AltLinux Packages. Позволяет получать уведомления о новых пакетах в репозитории
|
||||
AltLinux, поиска пакетов по названию, получения истории изменений, команды для установки пакета из репозитория и
|
||||
прочей информации о пакете.
|
||||
* `notes` - модуль заметок. Позволяет сохранять заметки для пользователей и чатов. Заметки являются ссылками на
|
||||
сообщения в чате.
|
||||
Стандартные блоки предоставляют базовые функции для работы бота
|
||||
|
||||
Список модулей будет пополняться. Идеи для модулей можно оставлять в [issues](https://gitflic.ru/project/armatik/ocab/issue/create).
|
||||
Полный перечень стандартных блоков:
|
||||
|
||||
## Установка бота
|
||||
- [`admin`](src/karkas_blocks/karkas_blocks/standard/admin/README.md) — блок модерирования чата;
|
||||
- [`roles`](src/karkas_blocks/karkas_blocks/standard/roles/README.md) — блок управления ролями пользователей;
|
||||
- [`config`](src/karkas_blocks/karkas_blocks/standard/config/README.md) — блок управления конфигурацией бота;
|
||||
- [`database`](src/karkas_blocks/karkas_blocks/standard/database/README.md) — блок для работы с базой данных;
|
||||
- [`fsm_database_storage`](src/karkas_blocks/karkas_blocks/standard/fsm_database_storage/README.md) — блок для хранения состояний FSM в базе данных;
|
||||
- [`filters`](src/karkas_blocks/karkas_blocks/standard/filters/README.md) — блок, предоставляющий фильтры для `aiogram`;
|
||||
- [`message_processing`](src/karkas_blocks/karkas_blocks/standard/message_processing/README.md) — блок обработки входящих сообщений;
|
||||
- [`miniapp`](src/karkas_blocks/karkas_blocks/standard/miniapp/README.md) — блок для реализации веб-интерфейса бота;
|
||||
- [`command_helper`](src/karkas_blocks/karkas_blocks/standard/command_helper/README.md) — блок для упрощения регистрации команд бота;
|
||||
- [`info`](src/karkas_blocks/karkas_blocks/standard/info/README.md) — блок предоставления информации о пользователях и чатах.
|
||||
|
||||
### Docker
|
||||
### Дополнительные официальные блоки
|
||||
|
||||
### Вручную
|
||||
Дополнительные официальные блоки созданы командой разработки платформы «Каркас» и предоставляют расширенные возможности для бота:
|
||||
|
||||
## Технологический стек
|
||||
|
||||
* Python 3.11.6 или выше - основной язык программирования.
|
||||
* SQLite 3 - база данных для хранения информации о чате и пользователях.
|
||||
* [Poetry](https://gitflic.ru/project/armatik/ocab/blob?file=how-to%20install%20deps.md&branch=OCAB-V2) - менеджер зависимостей.
|
||||
* aiogram 3 - библиотека для работы с Telegram API.
|
||||
* peewee - ORM для работы с базой данных.
|
||||
- [`yandexgpt`](src/karkas_blocks/karkas_blocks/external/yandexgpt/README.md) — блок для интеграции с нейросетью YandexGPT;
|
||||
- [`create_report_apps`](src/karkas_blocks/karkas_blocks/external/create_report_apps/README.md) — блок для создания отчётов об ошибках.
|
||||
|
105
docs/BLOCKS-SPEC.md
Normal file
105
docs/BLOCKS-SPEC.md
Normal file
@ -0,0 +1,105 @@
|
||||
# Спецификация блоков
|
||||
|
||||
> **Внимание!**
|
||||
>
|
||||
> Данная спецификация ещё не закончена и активно разрабатывается.
|
||||
>
|
||||
> Могут возникнуть изменения, которые не будут обратно совместимы (breaking changes).
|
||||
|
||||
Каждый блок представлен в виде папки, содержащей два обязательных файла: `info.json` и `__init__.py`.
|
||||
|
||||
## Метаданные блока (`info.json`)
|
||||
|
||||
Файл `info.json` содержит информацию о блоке в формате JSON. Пример структуры `info.json` приведён ниже:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "standard.info",
|
||||
"name": "Info",
|
||||
"description": "Блок с информацией",
|
||||
"author": "Karkas 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` | Объект, описывающий зависимости блока от других блоков |
|
||||
| `dependencies.required` и `dependencies.optional` | Объекты, описывающий обязательные и необязательные зависимости соответственно. Ключ — идентификатор блока, значение — версия или объект `DependencyInfo`. |
|
||||
| `pythonDependencies` | Объект, описывающий зависимости блока от внешних Python пакетов. |
|
||||
| `pythonDependencies.required` и `pythonDependencies.optional` | Объекты, описывающий обязательные и необязательные зависимости соответственно. Ключ — название пакета, значение — версия. |
|
||||
|
||||
### DependencyInfo
|
||||
|
||||
Объект `DependencyInfo` позволяет указать не только версию зависимости, но и список доступных к использованию атрибутов блока (`uses`). Если `uses` не указан, то доступ к блоку целиком запрещён. Пример объекта `DependencyInfo`:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "^1.0.0",
|
||||
"uses": ["db_api"]
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Описание |
|
||||
| :-------: | ----------------------------------- |
|
||||
| `version` | Версия блока |
|
||||
| `uses` | Список используемых атрибутов блока |
|
||||
|
||||
## Режимы работы блоков
|
||||
|
||||
### Непривилегированный режим (`privileged: false`)
|
||||
|
||||
- Исполнение блока происходит в доверенной среде на основе `RestrictedPython`, что накладывает ряд ограничений;
|
||||
- Может импортировать только явно разрешенные блоки, указанные в `pythonDependencies`, а также несколько стандартных блоков, необходимых для работы;
|
||||
- Имеет доступ к пакету `karkas_core.modules_system.public_api` для взаимодействия с ботом.
|
||||
|
||||
### Привилегированный режим (`privileged: true`)
|
||||
|
||||
- Блок исполняется без ограничений;
|
||||
- Имеет полный доступ ко всем пакетам, доступным в окружении;
|
||||
- Должен использоваться с осторожностью и только для блоков, требующих расширенных прав.
|
||||
|
||||
## Жизненный цикл блока
|
||||
|
||||
1. Загрузка метаданных из `info.json`;
|
||||
2. Проверка зависимостей:
|
||||
- Проверка всех обязательных зависимостей;
|
||||
- Проверка совместимости версий зависимостей;
|
||||
- Проверка зависимостей Python;
|
||||
3. Загрузка кода блока из `__init__.py`;
|
||||
4. Вызов функции `module_init`, если она есть;
|
||||
5. После загрузки всех блоков вызывается функция `module_late_init`, если она есть.
|
||||
|
||||
## Межблочное взаимодейтвие
|
||||
|
||||
Для блоков взаимодействия друг с другом описан [API](../src/karkas_core/karkas_core/modules_system/public_api/__init__.py),
|
||||
предоставляемое системой управления блоками.
|
||||
|
||||
Например, можно использовать функцию `get_module` для получения блока или предоставляемых им объекты по идентификатору.
|
42
docs/DEV.md
Normal file
42
docs/DEV.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Настройка рабочего окружения
|
||||
|
||||
Данная инструкция поможет вам настроить рабочее окружение для разработки Karkas.
|
||||
|
||||
## Предварительные требования
|
||||
|
||||
- **Python** — платформа «Каркас» требует интерпретатор языка Python версии 3.12;
|
||||
- **VSCode** — рекомендованная среда разработки;
|
||||
- **Git** — инструмент контроля версий, необходим клонирования репозиториянео.
|
||||
|
||||
## Шаги
|
||||
|
||||
1. Клонируйте репозиторий с помощью утилиты `git`:
|
||||
|
||||
```shell
|
||||
git clone https://gitflic.ru/project/alt-gnome/karkas.git
|
||||
```
|
||||
|
||||
2. Откройте папку `karkas` в VSCode. Среда разработки автоматически предложит открыть проект как Workspace, используя файл `karkas.code-workspace`. Нажмите `Открыть Workspace`, чтобы принять предложение;
|
||||
|
||||
3. Установите инструмент Poetry, следуя инструкциям из [официальной документации](https://python-poetry.org/docs/).
|
||||
|
||||
4. Выполните команду `poetry install` в корне проекта.
|
||||
|
||||
5. Выполните команду `soruce ./.venv/bin/activate` в корне проекта.
|
||||
|
||||
4. Выполните команду `pre-commit install -t commit-msg -t pre-commit` в корне проекта.
|
||||
|
||||
5. Для каждого пакета выполните следующую последовательность действий:
|
||||
|
||||
- Перейдите в папку пакета (например, `src/karkas_core`);
|
||||
- Выполните команду `poetry install`, чтобы установить зависимости пакета.
|
||||
|
||||
6. Выполните команду `soruce ./.venv/bin/activate` в папке пакета, над которым будут производится работы, чтобы активировать виртуальное окружение.
|
||||
|
||||
Теперь рабочее окружение настроено!
|
||||
|
||||
## Дополнительная информация
|
||||
|
||||
- Каждый пакет в монорепозитории имеет свой собственный файл `pyproject.toml`, где указаны его зависимости;
|
||||
- Poetry автоматически управляет виртуальными окружениями для каждого пакета;
|
||||
- Вы можете использовать команду `poetry add <package_name>` (где `<package_name>` — имя пакета) для добавления новых зависимостей.
|
48
gitflic-ci.yaml
Normal file
48
gitflic-ci.yaml
Normal file
@ -0,0 +1,48 @@
|
||||
stages:
|
||||
- lint
|
||||
- build
|
||||
|
||||
lint-pre-commit:
|
||||
stage: lint
|
||||
image: python:3.12-bullseye
|
||||
before_script:
|
||||
- export PIP_CACHE_DIR=$(pwd)/.cache/pip
|
||||
- export PRE_COMMIT_HOME=$(pwd)/.cache/pre-commit
|
||||
- pip install pre-commit
|
||||
scripts:
|
||||
- pre-commit run --all-files
|
||||
cache:
|
||||
paths:
|
||||
- .cache/
|
||||
|
||||
.docker-dev-build-template:
|
||||
before_script:
|
||||
- docker info
|
||||
- docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
|
||||
scripts:
|
||||
- |
|
||||
cd ./src/${KARKAS_PROJECT}
|
||||
export APP_VERSION=$(echo "$(git show -s --format=%ad --date=format:%Y.%m.%d $CI_COMMIT_SHA) (sha-$(git rev-parse --short=8 $CI_COMMIT_SHA))")
|
||||
export IMAGE_COMMIT=${IMAGE_NAME}:${CI_COMMIT_SHA}
|
||||
export IMAGE_BRANCH=${IMAGE_NAME}:$(echo $CI_COMMIT_REF_NAME | sed 's/[^a-zA-Z0-9]/-/g')
|
||||
docker build --build-arg APP_VERSION="$APP_VERSION" -t ${IMAGE_COMMIT} -t ${IMAGE_BRANCH} -f Dockerfile ../..
|
||||
docker push ${IMAGE_COMMIT}
|
||||
docker push ${IMAGE_BRANCH}
|
||||
|
||||
build-gnomik:
|
||||
stage: build
|
||||
image: docker:27.1.2
|
||||
variables:
|
||||
CI_REGISTRY: registry.gitflic.ru
|
||||
IMAGE_NAME: registry.gitflic.ru/project/alt-gnome/karkas/gnomik
|
||||
KARKAS_PROJECT: gnomik
|
||||
extends: .docker-dev-build-template
|
||||
|
||||
build-karkas-lite:
|
||||
stage: build
|
||||
image: docker:27.1.2
|
||||
variables:
|
||||
CI_REGISTRY: registry.gitflic.ru
|
||||
IMAGE_NAME: registry.gitflic.ru/project/alt-gnome/karkas/karkas-lite
|
||||
KARKAS_PROJECT: karkas_lite
|
||||
extends: .docker-dev-build-template
|
@ -1,47 +0,0 @@
|
||||
## Poetry
|
||||
|
||||
### Установка с официального сайта
|
||||
|
||||
```shell
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
```
|
||||
|
||||
### Установка с PyPi
|
||||
|
||||
```shell
|
||||
python3 -m pip install poetry
|
||||
```
|
||||
|
||||
Доп информация:https://www.8host.com/blog/ustanovka-menedzhera-zavisimostej-poetry/
|
||||
|
||||
## Зависимости
|
||||
|
||||
### Добавление зависимости
|
||||
```shell
|
||||
poetry add NAME
|
||||
```
|
||||
`NAME` - название зависимости.
|
||||
|
||||
### Установка зависимостей
|
||||
```shell
|
||||
poetry install
|
||||
```
|
||||
|
||||
### Обновление зависимостей
|
||||
```shell
|
||||
poetry update
|
||||
```
|
||||
|
||||
## Виртуальное окружение
|
||||
|
||||
### Создание/активация
|
||||
```shell
|
||||
poetry shell
|
||||
```
|
||||
|
||||
### Настройка
|
||||
|
||||
Хранить окружение внутри проекта
|
||||
```shell
|
||||
poetry config virtualenvs.in-project true
|
||||
```
|
15
init.py
15
init.py
@ -1,15 +0,0 @@
|
||||
from pathlib import Path
|
||||
from json import dumps
|
||||
|
||||
pwd = Path().cwd()
|
||||
dir_core = pwd / "src" / "core"
|
||||
dir_modules_standard = pwd / "src" / "modules" / "standard"
|
||||
dir_modules_custom = pwd / "src" / "modules" / "custom"
|
||||
|
||||
json = {
|
||||
'core': str(dir_core),
|
||||
'modules standard': str(dir_modules_standard),
|
||||
'modules custom': str(dir_modules_custom),
|
||||
}
|
||||
with open("src/paths.json", "w", encoding="utf8") as f:
|
||||
f.write(dumps(json, indent=4))
|
33
karkas.code-workspace
Normal file
33
karkas.code-workspace
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "Karkas Monorepo Root",
|
||||
"path": ".",
|
||||
},
|
||||
{
|
||||
"name": "Karkas Blocks",
|
||||
"path": "src/karkas_blocks"
|
||||
},
|
||||
{
|
||||
"name": "Karkas Core",
|
||||
"path": "src/karkas_core"
|
||||
},
|
||||
{
|
||||
"name": "Karkas Piccolo",
|
||||
"path": "src/karkas_piccolo"
|
||||
},
|
||||
{
|
||||
"name": "Gnomik",
|
||||
"path": "src/gnomik"
|
||||
},
|
||||
{
|
||||
"name": "Karkas Lite",
|
||||
"path": "src/karkas_lite"
|
||||
}
|
||||
],
|
||||
"extensions": {
|
||||
"recommendations": [
|
||||
"ms-python.python"
|
||||
]
|
||||
},
|
||||
}
|
1052
poetry.lock
generated
1052
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
2
poetry.toml
Normal file
2
poetry.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[virtualenvs]
|
||||
in-project = true
|
@ -1,24 +1,48 @@
|
||||
[tool.poetry]
|
||||
name = "ocab"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
name = "karkas-monorepo"
|
||||
version = "2.0.0"
|
||||
description = "Karkas 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",
|
||||
"Кирилл Уницаев <fiersik.kouji@yandex.ru>",
|
||||
"Максим Слипенко <maxim@slipenko.com>"
|
||||
]
|
||||
readme = "README.md"
|
||||
repository = "https://gitflic.ru/project/armatik/ocab"
|
||||
repository = "https://gitflic.ru/project/alt-gnome/karkas"
|
||||
packages = [
|
||||
{ include = "scripts" }
|
||||
]
|
||||
|
||||
[tool.poetry.urls]
|
||||
"Bug Tracker" = "https://gitflic.ru/project/alt-gnome/karkas/issue?status=OPEN"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
test = 'scripts.test:main'
|
||||
init = 'scripts.init:main'
|
||||
module = 'scripts.module:main'
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11.6"
|
||||
aiogram = "^3.2.0"
|
||||
peewee = "^3.17.0"
|
||||
pyyaml = "^6.0.1"
|
||||
requests = "^2.31.0"
|
||||
python = ">=3.10,<3.13"
|
||||
|
||||
[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"]
|
||||
|
21
scripts/init.py
Normal file
21
scripts/init.py
Normal file
@ -0,0 +1,21 @@
|
||||
from json import dumps
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
pwd = Path().cwd()
|
||||
dir_core = pwd / "src" / "karkas_core"
|
||||
dir_modules_standard = pwd / "src" / "karkas_blocks" / "standard"
|
||||
dir_modules_external = pwd / "src" / "karkas_blocks" / "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
115
scripts/module.py
Normal file
@ -0,0 +1,115 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
DEFAULTS = {
|
||||
"description": "Очень полезный модуль",
|
||||
"author": "Karkas Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": "false",
|
||||
}
|
||||
|
||||
|
||||
def create_module(args):
|
||||
module_dir = os.path.join("src/karkas_blocks/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
9
scripts/test.py
Normal file
@ -0,0 +1,9 @@
|
||||
import subprocess # nosec
|
||||
|
||||
|
||||
def main():
|
||||
subprocess.run(["python", "-u", "-m", "unittest", "discover"]) # nosec
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,2 +0,0 @@
|
||||
import src.service
|
||||
import src.core
|
@ -1,13 +0,0 @@
|
||||
TELEGRAM:
|
||||
TOKEN:
|
||||
|
||||
YANDEXGPT:
|
||||
TOKEN:
|
||||
CATALOGID:
|
||||
PROMPT:
|
||||
|
||||
ROLES:
|
||||
ADMIN: 0
|
||||
MODERATOR: 1
|
||||
USER: 2
|
||||
BOT: 3
|
@ -1,21 +0,0 @@
|
||||
TELEGRAM:
|
||||
TOKEN: xxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
APPROVED_CHAT_ID: "-123456789 | -012345678"
|
||||
ADMINCHATID: -12345678
|
||||
DEFAULT_CHAT_TAG: "@alt_gnome_chat"
|
||||
CHECK_BOT: True
|
||||
|
||||
YANDEXGPT:
|
||||
TOKEN: xxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
TOKEN_FOR_REQUEST: 8000
|
||||
TOKEN_FOR_ANSWER: 2000
|
||||
CATALOGID: xxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
PROMPT: "Ты чат-бот ..."
|
||||
STARTWORD: "Бот| Бот, | бот | бот,"
|
||||
INWORD: "помогите | не работает"
|
||||
|
||||
ROLES:
|
||||
ADMIN: 2
|
||||
MODERATOR: 1
|
||||
USER: 0
|
||||
BOT: 3
|
@ -1,24 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
async def check_log_file():
|
||||
# Проверка наличия файла для логов в формате log-dd-mm-yyyy.log
|
||||
# Если файл существует, то pass
|
||||
# Если файл не существует, то создаём его. файл лежит в директории src.core.log
|
||||
current_data = time.strftime("%d-%m-%Y")
|
||||
log_file = os.path.join(os.path.dirname(__file__), "log/", f"log-{current_data}.log")
|
||||
if not os.path.exists(log_file):
|
||||
with open(log_file, 'w') as file:
|
||||
file.write("Log file created\n")
|
||||
file.close()
|
||||
else:
|
||||
pass
|
||||
|
||||
async def log(message):
|
||||
await check_log_file()
|
||||
current_data = time.strftime("%d-%m-%Y")
|
||||
log_file = os.path.join(os.path.dirname(__file__), "log/", f"log-{current_data}.log")
|
||||
# print(log_file)
|
||||
with open(log_file, 'a') as file:
|
||||
file.write(f"{time.strftime('%H:%M:%S')} {message}\n")
|
||||
file.close()
|
@ -1,26 +0,0 @@
|
||||
from routers import include_routers
|
||||
from src.modules.standard.config.config import get_telegram_token
|
||||
from src.modules.standard.database.db_api import connect_database, create_tables
|
||||
|
||||
from asyncio import run
|
||||
from aiogram import Bot, Dispatcher
|
||||
|
||||
|
||||
async def main(bot: Bot):
|
||||
try:
|
||||
database, path = connect_database()
|
||||
database.connect()
|
||||
create_tables(database)
|
||||
|
||||
dp = Dispatcher()
|
||||
await include_routers(dp)
|
||||
await dp.start_polling(bot)
|
||||
|
||||
finally:
|
||||
await bot.session.close()
|
||||
database.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot = Bot(token=get_telegram_token())
|
||||
run(main(bot))
|
@ -1,18 +0,0 @@
|
||||
from aiogram import Dispatcher, Router, F, Bot
|
||||
from aiogram.types import Message
|
||||
|
||||
from src.modules.standard.info.routers import router as info_router
|
||||
from src.modules.standard.admin.routers import router as admin_router
|
||||
from src.modules.standard.message_processing.message_api import router as process_message
|
||||
from src.modules.standard.welcome.routers import router as welcome_router
|
||||
|
||||
|
||||
async def include_routers(dp: Dispatcher):
|
||||
"""
|
||||
Подключение роутеров в бота
|
||||
dp.include_router()
|
||||
"""
|
||||
dp.include_router(info_router)
|
||||
dp.include_router(admin_router)
|
||||
dp.include_router(process_message)
|
||||
|
38
src/gnomik/Dockerfile
Normal file
38
src/gnomik/Dockerfile
Normal file
@ -0,0 +1,38 @@
|
||||
FROM python:3.12-slim AS dependencies_installer
|
||||
|
||||
RUN pip install poetry
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./src/karkas_core/poetry* ./src/karkas_core/pyproject.toml /app/src/karkas_core/
|
||||
COPY ./src/karkas_blocks/poetry* ./src/karkas_blocks/pyproject.toml /app/src/karkas_blocks/
|
||||
COPY ./src/karkas_piccolo/poetry* ./src/karkas_piccolo/pyproject.toml /app/src/karkas_piccolo/
|
||||
COPY ./src/gnomik/poetry* ./src/gnomik/pyproject.toml /app/src/gnomik/
|
||||
|
||||
WORKDIR /app/src/gnomik
|
||||
|
||||
RUN poetry install --no-root --no-directory
|
||||
|
||||
FROM python:3.12-slim AS src
|
||||
|
||||
COPY ./src/karkas_core /app/src/karkas_core
|
||||
COPY ./src/karkas_blocks /app/src/karkas_blocks
|
||||
COPY ./src/karkas_piccolo /app/src/karkas_piccolo
|
||||
COPY ./src/gnomik /app/src/gnomik
|
||||
|
||||
FROM python:3.12-slim AS local_dependencies_installer
|
||||
|
||||
RUN pip install poetry
|
||||
|
||||
COPY --from=dependencies_installer /app/src/gnomik/.venv /app/src/gnomik/.venv
|
||||
COPY --from=src /app/src/ /app/src/
|
||||
WORKDIR /app/src/gnomik
|
||||
RUN poetry install
|
||||
|
||||
FROM python:3.12-slim AS base
|
||||
COPY --from=local_dependencies_installer /app/src/gnomik/.venv /app/src/gnomik/.venv
|
||||
COPY --from=src /app/src/ /app/src/
|
||||
WORKDIR /app/src/gnomik
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
CMD ["/bin/bash", "-c", ". .venv/bin/activate && python -m gnomik"]
|
14
src/gnomik/Dockerfile.dockerignore
Normal file
14
src/gnomik/Dockerfile.dockerignore
Normal file
@ -0,0 +1,14 @@
|
||||
**/Dockerfile
|
||||
**/*.dockerignore
|
||||
**/docker-compose.yml
|
||||
|
||||
**/.git
|
||||
**/.gitignore
|
||||
|
||||
**/.venv
|
||||
|
||||
**/.mypy_cache
|
||||
**/__pycache__/
|
||||
|
||||
src/gnomik/config.yaml
|
||||
src/gnomik/database/*
|
60
src/gnomik/README.md
Normal file
60
src/gnomik/README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Gnomик
|
||||
|
||||

|
||||
|
||||
Gnomик — это чат-бот для [ALT Gnome Chat](https://t.me/alt_gnome_chat), созданный на основе платформы «Каркас». Он предоставляет различные функции и возможности, помогающие членам сообщества ALT Gnome.
|
||||
|
||||
> ALT Gnome — открытое сообщество пользователей операционной системы ALT Regular Gnome. Платформы ALT Gnome:
|
||||
>
|
||||
> - [Wiki](https://alt-gnome.wiki)
|
||||
> - [Telegram-канал](https://t.me/alt_gnome)
|
||||
> - [Вконтакте](https://vk.com/alt_gnome)
|
||||
> - [Rutube](https://rutube.ru/channel/32425669/)
|
||||
> - [Платформа](https://plvideo.ru/@alt_gnome)
|
||||
> - [Boosty](https://boosty.to/alt_gnome)
|
||||
|
||||
## Функционал
|
||||
|
||||
<!--
|
||||
TODO: описать функционал
|
||||
-->
|
||||
|
||||
## Запуск
|
||||
|
||||
### Docker
|
||||
|
||||
1. Соберите Docker-образ:
|
||||
|
||||
```shell
|
||||
docker build -t gnomik .
|
||||
```
|
||||
|
||||
2. Запустите контейнер:
|
||||
|
||||
```shell
|
||||
docker run -p 9000:9000 -v ./config.yaml:/app/config.yaml -v ./database:/app/database gnomik
|
||||
```
|
||||
|
||||
Замените `./config.yaml` и `./database` на пути к локальным файлам конфигурации и пакам для базы данных.
|
||||
|
||||
### Вручную
|
||||
|
||||
1. Активируйте виртуальное окружение Gnomика:
|
||||
|
||||
```shell
|
||||
poetry shell
|
||||
```
|
||||
|
||||
2. Запустите бота:
|
||||
|
||||
```shell
|
||||
python -m gnomik
|
||||
```
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Конфигурация бота находится в файле `config.yaml`.
|
||||
|
||||
## Блоки
|
||||
|
||||
Список загружаемых блоков указан в файле `__main__.py`.
|
18
src/gnomik/config-example.yaml
Normal file
18
src/gnomik/config-example.yaml
Normal 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
|
12
src/gnomik/docker-compose.yml
Normal file
12
src/gnomik/docker-compose.yml
Normal 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
BIN
src/gnomik/docs/gnomik.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
33
src/gnomik/gnomik/__main__.py
Normal file
33
src/gnomik/gnomik/__main__.py
Normal file
@ -0,0 +1,33 @@
|
||||
import asyncio
|
||||
|
||||
from karkas_blocks import block_loader
|
||||
from karkas_core import Karkas
|
||||
|
||||
|
||||
async def main():
|
||||
ocab = Karkas()
|
||||
await ocab.init_app(
|
||||
[
|
||||
block_loader("standard", "config", safe=False),
|
||||
block_loader("standard", "filters", safe=False),
|
||||
block_loader("standard", "database", safe=False),
|
||||
block_loader("standard", "statistics", safe=False),
|
||||
block_loader("standard", "chats", safe=False),
|
||||
block_loader("standard", "users", safe=False),
|
||||
block_loader("standard", "command_helper"),
|
||||
block_loader("standard", "roles", safe=False),
|
||||
block_loader("standard", "fsm_database_storage", safe=False),
|
||||
block_loader("external", "create_report_apps"),
|
||||
block_loader("standard", "info"),
|
||||
block_loader("standard", "help"),
|
||||
# block_loader("external", "yandexgpt", safe=False),
|
||||
#
|
||||
# block_loader("standard", "admin"),
|
||||
# block_loader("standard", "message_processing"),
|
||||
# block_loader("standard", "miniapp", safe=False),
|
||||
]
|
||||
)
|
||||
await ocab.start()
|
||||
|
||||
|
||||
asyncio.run(main())
|
3134
src/gnomik/poetry.lock
generated
Normal file
3134
src/gnomik/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
src/gnomik/poetry.toml
Normal file
2
src/gnomik/poetry.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[virtualenvs]
|
||||
in-project = true
|
21
src/gnomik/pyproject.toml
Normal file
21
src/gnomik/pyproject.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[tool.poetry]
|
||||
name = "gnomik"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Максим Слипенко <maxim@slipenko.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<3.13"
|
||||
karkas-core = { extras=["webhook"], path = "../karkas_core", develop = true }
|
||||
karkas-blocks = { path = "../karkas_blocks", develop = true }
|
||||
karkas-piccolo = { path = "../karkas_piccolo", develop = true }
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[[tool.poetry.source]]
|
||||
name = "pytorch-cpu"
|
||||
url = "https://download.pytorch.org/whl/cpu"
|
||||
priority = "explicit"
|
10
src/karkas_blocks/README.md
Normal file
10
src/karkas_blocks/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Блоки Karkas
|
||||
|
||||
Блоки Karkas — это набор блоков для платформы «Каркас», которые добавляют функциональность ботам.
|
||||
|
||||
## Типы блоков
|
||||
|
||||
| Тип | Описание |
|
||||
| :-------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Стандартные (`standard`) | Блоки, содержащие основной функционал: управление пользователями, ролями и настройками |
|
||||
| Дополнительные (`external`) | Блоки, созданные командой разработки платформы «Каркас». Предоставляют расширенные возможности: интеграция с нейросетями, внешними сервисами и API |
|
1
src/karkas_blocks/karkas_blocks/__init__.py
Normal file
1
src/karkas_blocks/karkas_blocks/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .lib import block_loader
|
1
src/karkas_blocks/karkas_blocks/external/__init__.py
vendored
Normal file
1
src/karkas_blocks/karkas_blocks/external/__init__.py
vendored
Normal file
@ -0,0 +1 @@
|
||||
from . import yandexgpt
|
21
src/karkas_blocks/karkas_blocks/external/create_report_apps/README.md
vendored
Normal file
21
src/karkas_blocks/karkas_blocks/external/create_report_apps/README.md
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Блок Create Report Apps
|
||||
|
||||
Данный блок предназначен для помощи пользователям в создании отчётов об ошибках в приложениях.
|
||||
|
||||
## Функциональность
|
||||
|
||||
- Задаёт пользователю ряд вопросов, необходимых для составления отчёта;
|
||||
- Собирает информацию о системе пользователя;
|
||||
- Формирует отчёт в текстовом формате.
|
||||
|
||||
## Команды
|
||||
|
||||
| Команда | Описание |
|
||||
| :-------------------: | --------------- |
|
||||
| `/create_report_apps` | Создание отчёта |
|
||||
|
||||
## Использование
|
||||
|
||||
1. Отправьте команду `/create_report_apps` боту личным сообщением или в групповом чате;
|
||||
2. Ответьте на вопросы бота;
|
||||
3. Бот сформирует отчёт и отправит его.
|
1
src/karkas_blocks/karkas_blocks/external/create_report_apps/__init__.py
vendored
Normal file
1
src/karkas_blocks/karkas_blocks/external/create_report_apps/__init__.py
vendored
Normal file
@ -0,0 +1 @@
|
||||
from .main import module_init
|
140
src/karkas_blocks/karkas_blocks/external/create_report_apps/create_report.py
vendored
Normal file
140
src/karkas_blocks/karkas_blocks/external/create_report_apps/create_report.py
vendored
Normal file
@ -0,0 +1,140 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogram import Bot, Router
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import (
|
||||
BufferedInputFile,
|
||||
KeyboardButton,
|
||||
Message,
|
||||
ReplyKeyboardMarkup,
|
||||
ReplyKeyboardRemove,
|
||||
)
|
||||
|
||||
from karkas_core.modules_system.public_api import Utils, get_fsm_context
|
||||
|
||||
from .report import Report
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
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()
|
14
src/karkas_blocks/karkas_blocks/external/create_report_apps/info.json
vendored
Normal file
14
src/karkas_blocks/karkas_blocks/external/create_report_apps/info.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
115
src/karkas_blocks/karkas_blocks/external/create_report_apps/main.py
vendored
Normal file
115
src/karkas_blocks/karkas_blocks/external/create_report_apps/main.py
vendored
Normal 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 karkas_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", "Написать репорт о приложении")
|
59
src/karkas_blocks/karkas_blocks/external/create_report_apps/report.py
vendored
Normal file
59
src/karkas_blocks/karkas_blocks/external/create_report_apps/report.py
vendored
Normal 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
|
24
src/karkas_blocks/karkas_blocks/external/yandexgpt/README.md
vendored
Normal file
24
src/karkas_blocks/karkas_blocks/external/yandexgpt/README.md
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Блок YandexGPT
|
||||
|
||||
Данный блок интегрирует в бота нейросеть YandexGPT.
|
||||
|
||||
## Функциональность
|
||||
|
||||
- Позволяет боту отвечать на сообщения пользователей, используя YandexGPT;
|
||||
- Строит линию контекста для нейросети, используя историю сообщений.
|
||||
|
||||
## Конфигурация
|
||||
|
||||
| Параметр | Описание |
|
||||
| :--------------------: | --------------------------------------------------------------- |
|
||||
| `yandexgpt::token` | API-ключ для доступа к YandexGPT |
|
||||
| `yandexgpt::catalogid` | Идентификатор каталога YandexGPT |
|
||||
| `yandexgpt::prompt` | Системная подсказка для YandexGPT |
|
||||
| `yandexgpt::startword` | Слова, с которых должно начинаться сообщение, чтобы бот ответил |
|
||||
| `yandexgpt::inword` | Слова, которые должны быть в сообщении, чтобы бот ответил |
|
||||
|
||||
## Использование
|
||||
|
||||
1. Настройте конфигурационные параметры блока;
|
||||
2. Отправьте боту сообщение, начинающееся со слов или содержащее слова, указанные в параметрах `startword` и `inword` соответственно;
|
||||
3. Бот ответит на сообщение, генерируя текст с помощью YandexGPT.
|
2
src/karkas_blocks/karkas_blocks/external/yandexgpt/__init__.py
vendored
Normal file
2
src/karkas_blocks/karkas_blocks/external/yandexgpt/__init__.py
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
from .handlers import answer_to_message
|
||||
from .main import module_init
|
47
src/karkas_blocks/karkas_blocks/external/yandexgpt/handlers.py
vendored
Normal file
47
src/karkas_blocks/karkas_blocks/external/yandexgpt/handlers.py
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
# flake8: noqa
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import Message
|
||||
|
||||
from karkas_core.modules_system.public_api import get_module, log
|
||||
|
||||
from .yandexgpt import YandexGPT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from karkas_blocks.standard.config import IConfig
|
||||
from karkas_blocks.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")
|
21
src/karkas_blocks/karkas_blocks/external/yandexgpt/info.json
vendored
Normal file
21
src/karkas_blocks/karkas_blocks/external/yandexgpt/info.json
vendored
Normal 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": "*"
|
||||
}
|
||||
}
|
||||
}
|
50
src/karkas_blocks/karkas_blocks/external/yandexgpt/main.py
vendored
Normal file
50
src/karkas_blocks/karkas_blocks/external/yandexgpt/main.py
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from karkas_core.modules_system.public_api import get_module
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from karkas_blocks.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="помогите | не работает",
|
||||
)
|
11
src/karkas_blocks/karkas_blocks/external/yandexgpt/routers.py
vendored
Normal file
11
src/karkas_blocks/karkas_blocks/external/yandexgpt/routers.py
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# flake8: noqa
|
||||
from aiogram import F, Router
|
||||
|
||||
from .handlers import answer_to_message
|
||||
|
||||
router = Router()
|
||||
|
||||
# If the message starts with the word "Гномик" ("гномик") or responds to a bot message, `answer_to_message` is called
|
||||
router.message.register(
|
||||
answer_to_message, F.text.startswith("Гномик") | F.text.startswith("гномик")
|
||||
)
|
@ -1,24 +1,44 @@
|
||||
import requests
|
||||
# flake8: noqa
|
||||
import json
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from src.core.logger import log
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...standard.database import *
|
||||
from ...standard.config.config import *
|
||||
import aiohttp
|
||||
import requests
|
||||
|
||||
from karkas_core.modules_system.public_api import get_module, log
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from karkas_blocks.standard.config import IConfig
|
||||
from karkas_blocks.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": "белорусский язык",
|
||||
}
|
||||
"ru": "русский язык",
|
||||
"en": "английский язык",
|
||||
"de": "немецкий язык",
|
||||
"uk": "украинский язык",
|
||||
"es": "испанский язык",
|
||||
"be": "белорусский язык",
|
||||
}
|
||||
|
||||
def __init__(self, token, catalog_id):
|
||||
self.token = token
|
||||
@ -29,11 +49,13 @@ class YandexGPT:
|
||||
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):
|
||||
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}"
|
||||
"Authorization": f"Api-Key {self.token}",
|
||||
}
|
||||
answer_token = get_yandexgpt_token_for_answer()
|
||||
while True:
|
||||
@ -43,14 +65,16 @@ class YandexGPT:
|
||||
"completionOptions": {
|
||||
"stream": stream,
|
||||
"temperature": temperature,
|
||||
"maxTokens": max_tokens
|
||||
"maxTokens": max_tokens,
|
||||
},
|
||||
"messages": messages
|
||||
"messages": messages,
|
||||
}
|
||||
response = await self.async_request(url=url, headers=headers, prompt=request)
|
||||
except Exception as e: # TODO: Переделать обработку ошибок
|
||||
response = await self.async_request(
|
||||
url=url, headers=headers, prompt=request
|
||||
)
|
||||
except Exception as e: # TODO: Recreate error handling
|
||||
# print(e)
|
||||
await log(f"Error: {e}")
|
||||
log(f"Error: {e}")
|
||||
|
||||
continue
|
||||
if int(len(response["tokens"])) < (max_tokens - answer_token):
|
||||
@ -62,12 +86,19 @@ class YandexGPT:
|
||||
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):
|
||||
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}"
|
||||
"Authorization": f"Api-Key {self.token}",
|
||||
}
|
||||
|
||||
messages = [{"role": "system", "text": system_prompt}]
|
||||
@ -80,27 +111,30 @@ class YandexGPT:
|
||||
"completionOptions": {
|
||||
"stream": stream,
|
||||
"temperature": temperature,
|
||||
"maxTokens": max_tokens
|
||||
"maxTokens": max_tokens,
|
||||
},
|
||||
"messages": messages
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=prompt).text
|
||||
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=get_yandexgpt_token_for_request()
|
||||
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}"
|
||||
"Authorization": f"Api-Key {self.token}",
|
||||
}
|
||||
|
||||
messages = []
|
||||
@ -108,21 +142,24 @@ class YandexGPT:
|
||||
for message in input_messages:
|
||||
messages.append(message)
|
||||
|
||||
messages = await self.async_token_check(messages, gpt, max_tokens, stream, temperature)
|
||||
messages = await self.async_token_check(
|
||||
messages, gpt, max_tokens, stream, temperature
|
||||
)
|
||||
|
||||
request = {
|
||||
"modelUri": gpt,
|
||||
"completionOptions": {
|
||||
"stream": stream,
|
||||
"temperature": temperature,
|
||||
"maxTokens": max_tokens
|
||||
"maxTokens": max_tokens,
|
||||
},
|
||||
"messages": messages
|
||||
"messages": messages,
|
||||
}
|
||||
response = await self.async_request(url=url, headers=headers, prompt=request)
|
||||
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]
|
||||
@ -130,7 +167,9 @@ class YandexGPT:
|
||||
return await self.async_yandexgpt(
|
||||
f"Переведи на {output_language} сохранив оригинальный смысл текста. Верни только результат:",
|
||||
[{"role": "user", "text": text}],
|
||||
stream=False, temperature=0.6, max_tokens=8000
|
||||
stream=False,
|
||||
temperature=0.6,
|
||||
max_tokens=8000,
|
||||
)
|
||||
|
||||
async def async_yandexgpt_spelling_check(self, input_language, text):
|
||||
@ -140,15 +179,19 @@ class YandexGPT:
|
||||
f"Проверьте орфографию и пунктуацию текста на {input_language}. Верни исправленный текст "
|
||||
f"без смысловых искажений:",
|
||||
[{"role": "user", "text": text}],
|
||||
stream=False, temperature=0.6, max_tokens=8000
|
||||
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):
|
||||
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}"
|
||||
"Authorization": f"Api-Key {self.token}",
|
||||
}
|
||||
|
||||
messages = []
|
||||
@ -161,89 +204,119 @@ class YandexGPT:
|
||||
"completionOptions": {
|
||||
"stream": stream,
|
||||
"temperature": temperature,
|
||||
"maxTokens": max_tokens
|
||||
"maxTokens": max_tokens,
|
||||
},
|
||||
"messages": messages
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=prompt).text
|
||||
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):
|
||||
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
|
||||
# TODO: Make TTS function
|
||||
return 0
|
||||
|
||||
async def async_yandex_cloud_vision(self, image, features, language):
|
||||
# TODO: Сделать функцию Vision
|
||||
# TODO: Make Vision function
|
||||
return 0
|
||||
|
||||
async def collect_messages(self, message_id, chat_id):
|
||||
# Collect a chain of messages in the format:
|
||||
#
|
||||
# [
|
||||
# {"role": "user", "text": "<USER_NAME>: Hello!"},
|
||||
# {"role": "assistant", "text": "Hello!"}
|
||||
# ]
|
||||
|
||||
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))
|
||||
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):
|
||||
async def collecting_messages_for_history(
|
||||
self, start_message_id, end_message_id, chat_id
|
||||
):
|
||||
# Collect a chain of messages in the format:
|
||||
#
|
||||
# [
|
||||
# {"role": "user", "text": "<USER_NAME>: Hello!"},
|
||||
# {"role": "assistant", "text": "Hello!"}
|
||||
# ]
|
||||
|
||||
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))
|
||||
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):
|
||||
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
|
||||
stream=False,
|
||||
temperature=0.6,
|
||||
max_tokens=8000,
|
||||
)
|
||||
elif type == "yandexgpt":
|
||||
# print("yandexgpt_request")
|
||||
await log("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()
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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
|
||||
messages=messages, stream=False, temperature=0.6, max_tokens=8000
|
||||
)
|
||||
else:
|
||||
return "Ошибка: Неизвестный тип запроса | Error: Unknown request type"
|
25
src/karkas_blocks/karkas_blocks/lib.py
Normal file
25
src/karkas_blocks/karkas_blocks/lib.py
Normal file
@ -0,0 +1,25 @@
|
||||
import importlib
|
||||
import os
|
||||
|
||||
from karkas_core.modules_system.loaders.fs_loader import FSLoader
|
||||
from karkas_core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader
|
||||
|
||||
|
||||
def get_module_directory(module_name):
|
||||
spec = importlib.util.find_spec(module_name)
|
||||
if spec is None:
|
||||
raise ImportError(f"Module {module_name} not found")
|
||||
module_path = spec.origin
|
||||
if module_path is None:
|
||||
raise ImportError(f"Module {module_name} has no origin path")
|
||||
return os.path.dirname(module_path)
|
||||
|
||||
|
||||
karkas_blocks_path = get_module_directory("karkas_blocks")
|
||||
|
||||
|
||||
def block_loader(namespace: str, module_name: str, safe=True):
|
||||
if not safe:
|
||||
return UnsafeFSLoader(f"{karkas_blocks_path}/{namespace}/{module_name}")
|
||||
else:
|
||||
return FSLoader(f"{karkas_blocks_path}/{namespace}/{module_name}")
|
26
src/karkas_blocks/karkas_blocks/standard/admin/README.md
Normal file
26
src/karkas_blocks/karkas_blocks/standard/admin/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Блок Admin
|
||||
|
||||
Данный блок предоставляет администраторам и модераторам чата инструменты для управления.
|
||||
|
||||
## Функциональность
|
||||
|
||||
- Удаление сообщений;
|
||||
- Получение ID чата.
|
||||
|
||||
## Команды
|
||||
|
||||
| Команда | Описание |
|
||||
| :-------: | --------------------------------------------------------- |
|
||||
| `/rm` | Удаление сообщения, ответом на которое отправлена команда |
|
||||
| `/chatID` | Получение ID текущего чата |
|
||||
|
||||
## Использование
|
||||
|
||||
Удаление сообщений:
|
||||
|
||||
1. Ответьте на сообщение, которое нужно удалить;
|
||||
2. Отправьте команду `/rm`.
|
||||
|
||||
Получение ID чата:
|
||||
|
||||
1. Отправьте команду `/chatID`
|
@ -0,0 +1 @@
|
||||
from .main import module_init
|
65
src/karkas_blocks/karkas_blocks/standard/admin/handlers.py
Normal file
65
src/karkas_blocks/karkas_blocks/standard/admin/handlers.py
Normal file
@ -0,0 +1,65 @@
|
||||
# flake8: noqa
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import Message
|
||||
|
||||
from karkas_core.modules_system.public_api import get_module
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from karkas_blocks.standard.config import IConfig
|
||||
|
||||
config: "IConfig" = get_module("standard.config", "config")
|
||||
|
||||
|
||||
def get_default_chat_tag():
|
||||
return config.get("filters::default_chat_tag")
|
||||
|
||||
|
||||
async def delete_message(message: Message, bot: Bot):
|
||||
reply_message_id = message.reply_to_message.message_id
|
||||
await bot.delete_message(message.chat.id, reply_message_id)
|
||||
|
||||
|
||||
async def error_access(message: Message, bot: Bot):
|
||||
await message.reply("Вы не админ/модератор")
|
||||
|
||||
|
||||
async def get_chat_id(message: Message, bot: Bot):
|
||||
await message.reply(
|
||||
f"ID данного чата: `{message.chat.id}`", parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
|
||||
async def chat_not_in_approve_list(message: Message, bot: Bot):
|
||||
await message.reply(
|
||||
f"Бот недоступен в данном чате, пожалуйста,"
|
||||
f" обратитесь к администратору для добавления чата в список доступных или перейдите в чат "
|
||||
f"{get_default_chat_tag()}"
|
||||
)
|
||||
await get_chat_id(message, bot)
|
||||
|
||||
|
||||
async def mute_user(chat_id: int, user_id: int, time: int, bot: Bot):
|
||||
# TODO: type variable using `typing`
|
||||
mutePermissions = {
|
||||
"can_send_messages": False, # bool | None
|
||||
"can_send_audios": False, # bool | None
|
||||
"can_send_documents": False, # bool | None
|
||||
"can_send_photos": False, # bool | None
|
||||
"can_send_videos": False, # bool | None
|
||||
"can_send_video_notes": False, # bool | None
|
||||
"can_send_voice_notes": False, # bool | None
|
||||
"can_send_polls": False, # bool | None
|
||||
"can_send_other_messages": False, # bool | None
|
||||
"can_add_web_page_previews": False, # bool | None
|
||||
"can_change_info": False, # bool | None
|
||||
"can_invite_users": False, # bool | None
|
||||
"can_pin_messages": False, # bool | None
|
||||
"can_manage_topics": False, # bool | None
|
||||
# **extra_data: Any
|
||||
}
|
||||
end_time = time + int(time.time())
|
||||
await bot.restrict_chat_member(
|
||||
chat_id, user_id, until_date=end_time, **mutePermissions
|
||||
)
|
14
src/karkas_blocks/karkas_blocks/standard/admin/info.json
Normal file
14
src/karkas_blocks/karkas_blocks/standard/admin/info.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "standard.admin",
|
||||
"name": "Admin",
|
||||
"description": "Модуль для работы с админкой",
|
||||
"author": "Karkas Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": false,
|
||||
"dependencies": {
|
||||
"required": {
|
||||
"standard.filters": "^1.0.0",
|
||||
"standard.roles": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
7
src/karkas_blocks/karkas_blocks/standard/admin/main.py
Normal file
7
src/karkas_blocks/karkas_blocks/standard/admin/main.py
Normal file
@ -0,0 +1,7 @@
|
||||
from karkas_core.modules_system.public_api import register_router
|
||||
|
||||
from .routers import router
|
||||
|
||||
|
||||
async def module_init():
|
||||
register_router(router)
|
27
src/karkas_blocks/karkas_blocks/standard/admin/routers.py
Normal file
27
src/karkas_blocks/karkas_blocks/standard/admin/routers.py
Normal file
@ -0,0 +1,27 @@
|
||||
# flake8: noqa
|
||||
from aiogram import F, Router
|
||||
from aiogram.filters import Command
|
||||
|
||||
from karkas_core.modules_system.public_api import get_module, log
|
||||
|
||||
from .handlers import (
|
||||
chat_not_in_approve_list,
|
||||
delete_message,
|
||||
error_access,
|
||||
get_chat_id,
|
||||
)
|
||||
|
||||
(ChatModerOrAdminFilter, ChatNotInApproveFilter) = get_module(
|
||||
"standard.filters", ["ChatModerOrAdminFilter", "ChatNotInApproveFilter"]
|
||||
)
|
||||
|
||||
router = Router()
|
||||
|
||||
# If message is not empty and the `ChatNotInApproveFilter` is applied, then the `chat_not_in_approve_list` function is called
|
||||
router.message.register(chat_not_in_approve_list, ChatNotInApproveFilter(), F.text)
|
||||
|
||||
router.message.register(get_chat_id, ChatModerOrAdminFilter(), Command("chatID"))
|
||||
|
||||
router.message.register(delete_message, ChatModerOrAdminFilter(), Command("rm"))
|
||||
router.message.register(error_access, Command("rm"))
|
||||
router.message.register(error_access, Command("chatID"))
|
15
src/karkas_blocks/karkas_blocks/standard/chats/__init__.py
Normal file
15
src/karkas_blocks/karkas_blocks/standard/chats/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
from karkas_core.modules_system.public_api import (
|
||||
get_module,
|
||||
register_outer_message_middleware,
|
||||
)
|
||||
|
||||
from .main import ChatsMiddleware
|
||||
|
||||
|
||||
def module_init():
|
||||
register_app_config = get_module("standard.database", "register_app_config")
|
||||
from .db import APP_CONFIG
|
||||
|
||||
register_app_config(APP_CONFIG)
|
||||
|
||||
register_outer_message_middleware(ChatsMiddleware())
|
@ -0,0 +1 @@
|
||||
from .piccolo_app import APP_CONFIG
|
@ -0,0 +1,15 @@
|
||||
import os
|
||||
|
||||
from karkas_piccolo.conf.apps import AppConfig
|
||||
|
||||
from .tables import ChatInfo
|
||||
|
||||
CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
APP_CONFIG = AppConfig(
|
||||
app_name="standard.chats",
|
||||
migrations_folder_path=os.path.join(CURRENT_DIRECTORY, "piccolo_migrations"),
|
||||
table_classes=[ChatInfo],
|
||||
migration_dependencies=[],
|
||||
commands=[],
|
||||
)
|
@ -0,0 +1,104 @@
|
||||
from piccolo.apps.migrations.auto.migration_manager import MigrationManager
|
||||
from piccolo.columns.column_types import Date, Integer, Text
|
||||
from piccolo.columns.defaults.date import DateNow
|
||||
from piccolo.columns.indexes import IndexMethod
|
||||
|
||||
ID = "2024-08-20T17:25:21:296396"
|
||||
VERSION = "1.16.0"
|
||||
DESCRIPTION = ""
|
||||
|
||||
|
||||
async def forwards():
|
||||
manager = MigrationManager(
|
||||
migration_id=ID, app_name="standard.chats", description=DESCRIPTION
|
||||
)
|
||||
|
||||
manager.add_table(
|
||||
class_name="ChatInfo", tablename="chat_info", schema=None, columns=None
|
||||
)
|
||||
|
||||
manager.add_column(
|
||||
table_class_name="ChatInfo",
|
||||
tablename="chat_info",
|
||||
column_name="chat_id",
|
||||
db_column_name="chat_id",
|
||||
column_class_name="Integer",
|
||||
column_class=Integer,
|
||||
params={
|
||||
"default": 0,
|
||||
"null": False,
|
||||
"primary_key": True,
|
||||
"unique": False,
|
||||
"index": False,
|
||||
"index_method": IndexMethod.btree,
|
||||
"choices": None,
|
||||
"db_column_name": None,
|
||||
"secret": False,
|
||||
},
|
||||
schema=None,
|
||||
)
|
||||
|
||||
manager.add_column(
|
||||
table_class_name="ChatInfo",
|
||||
tablename="chat_info",
|
||||
column_name="chat_name",
|
||||
db_column_name="chat_name",
|
||||
column_class_name="Text",
|
||||
column_class=Text,
|
||||
params={
|
||||
"default": "",
|
||||
"null": False,
|
||||
"primary_key": False,
|
||||
"unique": False,
|
||||
"index": False,
|
||||
"index_method": IndexMethod.btree,
|
||||
"choices": None,
|
||||
"db_column_name": None,
|
||||
"secret": False,
|
||||
},
|
||||
schema=None,
|
||||
)
|
||||
|
||||
manager.add_column(
|
||||
table_class_name="ChatInfo",
|
||||
tablename="chat_info",
|
||||
column_name="chat_type",
|
||||
db_column_name="chat_type",
|
||||
column_class_name="Integer",
|
||||
column_class=Integer,
|
||||
params={
|
||||
"default": 10,
|
||||
"null": False,
|
||||
"primary_key": False,
|
||||
"unique": False,
|
||||
"index": False,
|
||||
"index_method": IndexMethod.btree,
|
||||
"choices": None,
|
||||
"db_column_name": None,
|
||||
"secret": False,
|
||||
},
|
||||
schema=None,
|
||||
)
|
||||
|
||||
manager.add_column(
|
||||
table_class_name="ChatInfo",
|
||||
tablename="chat_info",
|
||||
column_name="created_at",
|
||||
db_column_name="created_at",
|
||||
column_class_name="Date",
|
||||
column_class=Date,
|
||||
params={
|
||||
"default": DateNow(),
|
||||
"null": False,
|
||||
"primary_key": False,
|
||||
"unique": False,
|
||||
"index": False,
|
||||
"index_method": IndexMethod.btree,
|
||||
"choices": None,
|
||||
"db_column_name": None,
|
||||
"secret": False,
|
||||
},
|
||||
schema=None,
|
||||
)
|
||||
|
||||
return manager
|
@ -0,0 +1,9 @@
|
||||
from piccolo.columns import Date, Integer, Text
|
||||
from piccolo.table import Table
|
||||
|
||||
|
||||
class ChatInfo(Table):
|
||||
chat_id = Integer(primary_key=True)
|
||||
chat_name = Text()
|
||||
chat_type = Integer(default=10)
|
||||
created_at = Date()
|
13
src/karkas_blocks/karkas_blocks/standard/chats/info.json
Normal file
13
src/karkas_blocks/karkas_blocks/standard/chats/info.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "standard.chats",
|
||||
"name": "Чаты",
|
||||
"description": "Очень полезный модуль",
|
||||
"author": "Karkas Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": true,
|
||||
"dependencies": {
|
||||
"required": {
|
||||
"standard.database": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
36
src/karkas_blocks/karkas_blocks/standard/chats/main.py
Normal file
36
src/karkas_blocks/karkas_blocks/standard/chats/main.py
Normal file
@ -0,0 +1,36 @@
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
|
||||
from .db.tables import ChatInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiogram.types import Chat, TelegramObject
|
||||
|
||||
|
||||
async def update_chat_info(chat: "Chat"):
|
||||
chat_name = chat.title if chat.type != "private" else ""
|
||||
|
||||
await ChatInfo.insert(
|
||||
ChatInfo(
|
||||
chat_name=chat_name,
|
||||
)
|
||||
).on_conflict(
|
||||
action="DO UPDATE",
|
||||
values=[
|
||||
ChatInfo.chat_name,
|
||||
],
|
||||
).run()
|
||||
|
||||
|
||||
class ChatsMiddleware(BaseMiddleware):
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[["TelegramObject", Dict[str, Any]], Awaitable[Any]],
|
||||
event: "TelegramObject",
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
chat = event.chat
|
||||
await update_chat_info(chat)
|
||||
result = await handler(event, data)
|
||||
return result
|
@ -0,0 +1,23 @@
|
||||
# Блок Command Helper
|
||||
|
||||
Данный блок упрощает регистрации команд бота и управления ими.
|
||||
|
||||
## Функциональность
|
||||
|
||||
- Регистрация команд бота;
|
||||
- Установка команд для пользователей в зависимости от их роли.
|
||||
|
||||
## Использование
|
||||
|
||||
1. Импортируйте функцию `register_command`;
|
||||
2. Вызовите функцию `register_command`, передав ей название команды, её описание и роль пользователей, которым доступна она будет доступна.
|
||||
|
||||
## Пример
|
||||
|
||||
```python
|
||||
from karkas_core.modules_system.public_api import get_module
|
||||
|
||||
register_command = get_module("standard.command_helper", "register_command")
|
||||
|
||||
register_command("my_command", "Описание моей команды", role="ADMIN")
|
||||
```
|
@ -0,0 +1 @@
|
||||
from .main import get_user_commands, module_late_init, register_command
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "standard.command_helper",
|
||||
"name": "Command helper",
|
||||
"description": "Модуль для отображения команд при вводе '/'",
|
||||
"author": "Karkas Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": false,
|
||||
"dependencies": {}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
from aiogram.types import BotCommand
|
||||
|
||||
from karkas_core.modules_system.public_api import set_my_commands
|
||||
|
||||
commands = dict()
|
||||
|
||||
|
||||
def register_command(command, description, long_description=None, role="USER"):
|
||||
if long_description is None:
|
||||
long_description = description
|
||||
|
||||
if role not in commands:
|
||||
commands[role] = dict()
|
||||
commands[role][command] = {
|
||||
"description": description,
|
||||
"long_description": long_description,
|
||||
}
|
||||
|
||||
|
||||
async def set_commands(role="USER"):
|
||||
bot_commands = []
|
||||
if role in commands:
|
||||
user_commands = commands[role]
|
||||
for command in user_commands:
|
||||
bot_commands.append(
|
||||
BotCommand(
|
||||
command=command,
|
||||
description=user_commands[command]["description"],
|
||||
)
|
||||
)
|
||||
|
||||
await set_my_commands(
|
||||
bot_commands,
|
||||
)
|
||||
|
||||
|
||||
def get_commands(role="USER"):
|
||||
if role in commands:
|
||||
return commands[role].copy()
|
||||
return {}
|
||||
|
||||
|
||||
async def set_user_commands():
|
||||
await set_commands("USER")
|
||||
|
||||
|
||||
def get_user_commands():
|
||||
return get_commands("USER")
|
||||
|
||||
|
||||
async def module_late_init():
|
||||
await set_user_commands()
|
28
src/karkas_blocks/karkas_blocks/standard/config/README.md
Normal file
28
src/karkas_blocks/karkas_blocks/standard/config/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Блок Config
|
||||
|
||||
Данный блок позволяет управляет конфигурацией бота.
|
||||
|
||||
## Функциональность
|
||||
|
||||
- Загрузка конфигурации из файла `config.yaml`;
|
||||
- Сохранение конфигурации в файл;
|
||||
- Регистрация параметров конфигурации;
|
||||
- Получение значений параметров.
|
||||
|
||||
## Использование
|
||||
|
||||
1. Импортируйте объект `config`;
|
||||
2. Вызовите метод `register`, чтобы зарегистрировать параметр конфигурации;
|
||||
3. Вызовите метод `get`, чтобы получить значение параметра.
|
||||
|
||||
## Пример
|
||||
|
||||
```python
|
||||
from karkas_core.modules_system.public_api import get_module
|
||||
|
||||
config = get_module("standard.config", "config")
|
||||
|
||||
config.register("my_parameter", "string", default_value="default")
|
||||
|
||||
value = config.get("my_parameter")
|
||||
```
|
@ -0,0 +1,2 @@
|
||||
from .config import IConfig, config
|
||||
from .main import module_late_init
|
@ -0,0 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
from .config_manager import ConfigManager
|
||||
|
||||
IConfig = ConfigManager
|
||||
|
||||
config: ConfigManager = ConfigManager(config_path="config.yaml")
|
@ -0,0 +1,115 @@
|
||||
import inspect
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
def __init__(self, config_path: str):
|
||||
self.config_path = config_path
|
||||
|
||||
self._config: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
self._metadata: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def load(self, file_path: str = ""):
|
||||
if not file_path:
|
||||
file_path = self.config_path
|
||||
|
||||
def build_key(prev, next):
|
||||
if prev:
|
||||
return f"{prev}::{next}"
|
||||
return next
|
||||
|
||||
def recurse_set(value, key=""):
|
||||
if isinstance(value, dict):
|
||||
for k, v in value.items():
|
||||
recurse_set(v, build_key(key, k))
|
||||
return
|
||||
if key in self._metadata:
|
||||
self._config[key] = value
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
data = yaml.safe_load(file)
|
||||
recurse_set(data)
|
||||
|
||||
def save(self, file_path: str = ""):
|
||||
if not file_path:
|
||||
file_path = self.config_path
|
||||
|
||||
def nested_dict(flat_dict):
|
||||
result = {}
|
||||
for key, value in flat_dict.items():
|
||||
keys = key.split("::")
|
||||
d = result
|
||||
for k in keys[:-1]:
|
||||
d = d.setdefault(k, {})
|
||||
d[keys[-1]] = value
|
||||
return result
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as file:
|
||||
yaml.dump(nested_dict(self._config), file, allow_unicode=True)
|
||||
|
||||
def _check_rights(self, key, module_id, access_type="get"):
|
||||
return
|
||||
|
||||
def get(self, key: str):
|
||||
module_id = self._get_module_id()
|
||||
self._check_rights(key, module_id)
|
||||
return self._config.get(key, self._metadata.get(key).get("default_value"))
|
||||
|
||||
def get_meta(self, key: str):
|
||||
module_id = self._get_module_id()
|
||||
self._check_rights(key, module_id, "get_meta")
|
||||
return self._metadata.get(key)
|
||||
|
||||
def _get_module_id(self):
|
||||
caller_frame = inspect.currentframe().f_back.f_back
|
||||
caller_globals = caller_frame.f_globals
|
||||
module_id = caller_globals.get("__karkas_block_id__")
|
||||
return module_id
|
||||
|
||||
def mass_set(self, updates: Dict[str, Any]):
|
||||
module_id = self._get_module_id()
|
||||
for key, value in updates.items():
|
||||
self._check_rights(key, module_id, "set")
|
||||
if key in self._metadata:
|
||||
# TODO: add metadata-based type and value validation
|
||||
self._config[key] = value
|
||||
else:
|
||||
raise KeyError(f"Key {key} is not registered.")
|
||||
|
||||
def register(
|
||||
self,
|
||||
key: str,
|
||||
value_type: str,
|
||||
options: List[Any] = None,
|
||||
multiple: bool = False,
|
||||
default_value=None,
|
||||
editable: bool = True,
|
||||
shared: bool = False,
|
||||
required: bool = False,
|
||||
visible: bool = True,
|
||||
pretty_name: str = "",
|
||||
description: str = "",
|
||||
):
|
||||
module_id = self._get_module_id()
|
||||
|
||||
self._check_rights(key, module_id, "register")
|
||||
|
||||
if key in self._metadata:
|
||||
raise ValueError("ERROR")
|
||||
|
||||
self._metadata[key] = {
|
||||
"type": value_type,
|
||||
"multiple": multiple,
|
||||
"options": options,
|
||||
"default_value": default_value,
|
||||
"visible": visible,
|
||||
"editable": editable,
|
||||
"shared": shared,
|
||||
"required": required,
|
||||
"pretty_name": pretty_name,
|
||||
"description": description,
|
||||
"module_id": module_id,
|
||||
}
|
18
src/karkas_blocks/karkas_blocks/standard/config/info.json
Normal file
18
src/karkas_blocks/karkas_blocks/standard/config/info.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "standard.config",
|
||||
"name": "Config YAML",
|
||||
"description": "Модуль для работы с конфигурационным файлом бота (YAML)",
|
||||
"author": "Karkas Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": true,
|
||||
"dependencies": {
|
||||
"optional": {
|
||||
"standard.miniapp": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"pythonDependencies": {
|
||||
"optional": {
|
||||
"flet": "^0.23.2"
|
||||
}
|
||||
}
|
||||
}
|
30
src/karkas_blocks/karkas_blocks/standard/config/main.py
Normal file
30
src/karkas_blocks/karkas_blocks/standard/config/main.py
Normal file
@ -0,0 +1,30 @@
|
||||
from karkas_core.modules_system.public_api import get_module, log
|
||||
|
||||
from .config import config
|
||||
from .miniapp_ui import get_miniapp_blueprint
|
||||
|
||||
|
||||
def register_settings_page():
|
||||
try:
|
||||
register_page = get_module("standard.miniapp", "register_page")
|
||||
|
||||
prefix = "settings"
|
||||
|
||||
register_page(
|
||||
name="Настройки",
|
||||
path="/settings",
|
||||
blueprint=get_miniapp_blueprint(config, prefix),
|
||||
prefix=prefix,
|
||||
role="ADMIN",
|
||||
)
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
log(str(e))
|
||||
pass
|
||||
|
||||
|
||||
def module_late_init():
|
||||
register_settings_page()
|
||||
|
||||
pass
|
267
src/karkas_blocks/karkas_blocks/standard/config/miniapp_ui.py
Normal file
267
src/karkas_blocks/karkas_blocks/standard/config/miniapp_ui.py
Normal file
@ -0,0 +1,267 @@
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
try:
|
||||
import dash_bootstrap_components as dbc
|
||||
import flask
|
||||
from dash_extensions.enrich import ALL, Input, Output, State, dcc, html
|
||||
|
||||
DASH_AVAILABLE = True
|
||||
except ImportError:
|
||||
DASH_AVAILABLE = False
|
||||
|
||||
from karkas_core.modules_system.public_api import get_module
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from karkas_blocks.standard.roles import Roles as IRoles
|
||||
|
||||
from .config_manager import ConfigManager
|
||||
|
||||
|
||||
def create_control(key: str, config: "ConfigManager"):
|
||||
value = config.get(key)
|
||||
meta = config.get_meta(key)
|
||||
|
||||
component_id = {
|
||||
"type": "setting",
|
||||
"key": key,
|
||||
}
|
||||
|
||||
label_text = meta.get("pretty_name") or key
|
||||
|
||||
if meta.get("type") in ["string", "int", "float", "password"]:
|
||||
input_type = {
|
||||
"string": "text",
|
||||
"int": "number",
|
||||
"float": "number",
|
||||
"password": "password",
|
||||
}.get(meta.get("type"), "text")
|
||||
|
||||
input_props = {
|
||||
"id": component_id,
|
||||
"type": input_type,
|
||||
}
|
||||
|
||||
if meta.get("type") != "password":
|
||||
input_props["value"] = value
|
||||
|
||||
if meta.get("type") == "int":
|
||||
input_props["step"] = 1
|
||||
input_props["pattern"] = r"\d+"
|
||||
elif meta.get("type") == "float":
|
||||
input_props["step"] = "any"
|
||||
|
||||
component = dbc.Input(**input_props, invalid=False)
|
||||
|
||||
elif meta.get("type") == "select":
|
||||
options = [{"label": opt, "value": opt} for opt in meta.get("options", [])]
|
||||
component = dcc.Dropdown(
|
||||
id=component_id,
|
||||
options=options,
|
||||
value=value,
|
||||
style={
|
||||
"padding-left": 0,
|
||||
"padding-right": 0,
|
||||
},
|
||||
)
|
||||
elif meta.get("type") == "checkbox":
|
||||
component = dbc.Checkbox(
|
||||
id=component_id,
|
||||
checked=value,
|
||||
label=label_text,
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
row = []
|
||||
if meta.get("type") != "checkbox":
|
||||
row.append(dbc.Label(label_text))
|
||||
|
||||
row.append(component)
|
||||
|
||||
if meta.get("description"):
|
||||
row.append(dbc.FormText(meta.get("description")))
|
||||
|
||||
return dbc.Row(row, className="mb-3 mx-1")
|
||||
|
||||
|
||||
def build_settings_tree(config: "ConfigManager"):
|
||||
tree = {}
|
||||
|
||||
for key, value in config._metadata.items():
|
||||
if not value["visible"]:
|
||||
continue
|
||||
|
||||
parts = key.split("::")
|
||||
control = create_control(key, config)
|
||||
|
||||
current = tree
|
||||
for i, part in enumerate(parts[:-1]):
|
||||
if part not in current:
|
||||
current[part] = {"__controls": []}
|
||||
current = current[part]
|
||||
|
||||
current["__controls"].append(control)
|
||||
|
||||
return tree
|
||||
|
||||
|
||||
def create_card(category, controls):
|
||||
return dbc.Card(
|
||||
[
|
||||
dbc.CardHeader(html.H3(category, className="mb-0")),
|
||||
dbc.CardBody(controls),
|
||||
],
|
||||
className="mb-3",
|
||||
)
|
||||
|
||||
|
||||
def create_settings_components(tree, level=0):
|
||||
components = []
|
||||
|
||||
for category, subtree in tree.items():
|
||||
if category == "__controls":
|
||||
continue
|
||||
|
||||
controls = subtree.get("__controls", [])
|
||||
subcomponents = create_settings_components(subtree, level + 1)
|
||||
|
||||
if controls or subcomponents:
|
||||
card_content = controls + subcomponents
|
||||
card = create_card(category, card_content)
|
||||
components.append(card)
|
||||
|
||||
return components
|
||||
|
||||
|
||||
def get_miniapp_blueprint(config: "ConfigManager", prefix: str):
|
||||
Roles: "type[IRoles]" = get_module("standard.roles", "Roles")
|
||||
|
||||
roles = Roles()
|
||||
|
||||
import datetime
|
||||
|
||||
from dash_extensions.enrich import DashBlueprint
|
||||
|
||||
bp = DashBlueprint()
|
||||
|
||||
def create_layout():
|
||||
settings_tree = build_settings_tree(config)
|
||||
settings_components = create_settings_components(settings_tree)
|
||||
|
||||
layout = html.Div(
|
||||
[
|
||||
html.Script(),
|
||||
html.H1("Настройки"),
|
||||
dbc.Form(settings_components),
|
||||
html.Div(id="save-confirmation"),
|
||||
dbc.Button(
|
||||
"Сохранить",
|
||||
id="save-settings",
|
||||
color="primary",
|
||||
className="mt-3 w-100",
|
||||
n_clicks=0,
|
||||
),
|
||||
html.Div(id="settings-update-trigger", style={"display": "none"}),
|
||||
dcc.Store(id="settings-store"),
|
||||
],
|
||||
style={
|
||||
"padding": "20px",
|
||||
},
|
||||
)
|
||||
return layout
|
||||
|
||||
bp.layout = create_layout
|
||||
|
||||
@bp.callback(
|
||||
Output("save-confirmation", "children"),
|
||||
Output("settings-store", "data"),
|
||||
Input("save-settings", "n_clicks"),
|
||||
State({"type": "setting", "key": ALL}, "value"),
|
||||
State({"type": "setting", "key": ALL}, "id"),
|
||||
prevent_initial_call=True,
|
||||
allow_duplicate=True,
|
||||
)
|
||||
def save_settings(n_clicks, values, keys):
|
||||
if n_clicks > 0:
|
||||
user = getattr(flask.g, "user", None)
|
||||
|
||||
if user is None:
|
||||
return (
|
||||
dbc.Alert(
|
||||
"Вы не авторизованы!",
|
||||
color="danger",
|
||||
duration=10000,
|
||||
),
|
||||
"-",
|
||||
)
|
||||
|
||||
if not asyncio.run(roles.check_admin_permission(user["id"])):
|
||||
return (
|
||||
dbc.Alert(
|
||||
"Вы не администратор!",
|
||||
color="danger",
|
||||
duration=10000,
|
||||
),
|
||||
"-",
|
||||
)
|
||||
|
||||
# TODO: add values validation
|
||||
|
||||
updated_settings = {}
|
||||
for value, id_dict in zip(values, keys):
|
||||
key: str = id_dict["key"]
|
||||
if prefix:
|
||||
key = key.removeprefix(f"{prefix}-")
|
||||
|
||||
meta = config.get_meta(key)
|
||||
|
||||
if meta["type"] == "password":
|
||||
# Is updated only if new value is specified
|
||||
if value:
|
||||
updated_settings[key] = value
|
||||
else:
|
||||
updated_settings[key] = value
|
||||
|
||||
config.mass_set(updated_settings)
|
||||
config.save()
|
||||
|
||||
now = datetime.datetime.now()
|
||||
date_str = now.strftime("%H:%M:%S")
|
||||
|
||||
return (
|
||||
dbc.Alert(
|
||||
f"Настройки сохранены в {date_str}",
|
||||
color="success",
|
||||
duration=10000,
|
||||
),
|
||||
date_str,
|
||||
)
|
||||
|
||||
bp.clientside_callback(
|
||||
"""
|
||||
function(n_clicks) {
|
||||
const buttonSelector = '#%s-save-settings';
|
||||
if (n_clicks > 0) {
|
||||
document.querySelector(buttonSelector).disabled = true;
|
||||
}
|
||||
}
|
||||
"""
|
||||
% (prefix),
|
||||
Input("save-settings", "n_clicks"),
|
||||
)
|
||||
|
||||
bp.clientside_callback(
|
||||
"""
|
||||
function(data) {
|
||||
const buttonSelector = '#%s-save-settings';
|
||||
if (data) {
|
||||
document.querySelector(buttonSelector).disabled = false;
|
||||
}
|
||||
}
|
||||
"""
|
||||
% (prefix),
|
||||
Input("settings-store", "data"),
|
||||
)
|
||||
|
||||
return bp
|
@ -1,6 +1,7 @@
|
||||
from src.modules.standard.config.config import get_config
|
||||
import unittest
|
||||
|
||||
from src.karkas_blocks.standard.config.config import get_config
|
||||
|
||||
yaml_load = get_config(is_test=True)
|
||||
|
||||
|
||||
@ -18,20 +19,27 @@ class TestConfig(unittest.TestCase):
|
||||
def test_yaml_keys_existence(self):
|
||||
self.assertTrue(all(key in yaml_load for key in ["TELEGRAM", "ROLES"]))
|
||||
self.assertIn("TOKEN", yaml_load["TELEGRAM"])
|
||||
self.assertTrue(all(role in yaml_load["ROLES"] for role in ["ADMIN", "MODERATOR", "USER"]))
|
||||
self.assertTrue(
|
||||
all(role in yaml_load["ROLES"] for role in ["ADMIN", "MODERATOR", "USER"])
|
||||
)
|
||||
|
||||
def test_yaml_yaml_load_types(self):
|
||||
self.assertIsInstance(yaml_load["TELEGRAM"]["TOKEN"], str)
|
||||
self.assertTrue(all(isinstance(yaml_load["ROLES"][role], int) for role in ["ADMIN", "MODERATOR", "USER"]))
|
||||
self.assertTrue(
|
||||
all(
|
||||
isinstance(yaml_load["ROLES"][role], int)
|
||||
for role in ["ADMIN", "MODERATOR", "USER"]
|
||||
)
|
||||
)
|
||||
|
||||
def test_yaml_values(self):
|
||||
expected_token = 'xxxxxxxxxxxxxxxxxxxx'
|
||||
expected_role_values = {'ADMIN': 0, 'MODERATOR': 1, 'USER': 2, 'BOT': 3}
|
||||
expected_token = "xxxxxxxxxxxxxxxxxxxx" # nosec
|
||||
expected_role_values = {"ADMIN": 0, "MODERATOR": 1, "USER": 2, "BOT": 3}
|
||||
|
||||
self.assertEqual(yaml_load["TELEGRAM"]["TOKEN"], expected_token)
|
||||
for role, value in expected_role_values.items():
|
||||
self.assertEqual(yaml_load["ROLES"][role], value)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
@ -0,0 +1 @@
|
||||
from .main import module_init, module_late_init, register_app_config
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "standard.database",
|
||||
"name": "Database",
|
||||
"description": "Модуль для работы с БД",
|
||||
"author": "Karkas Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": true,
|
||||
"dependencies": {}
|
||||
}
|
40
src/karkas_blocks/karkas_blocks/standard/database/main.py
Normal file
40
src/karkas_blocks/karkas_blocks/standard/database/main.py
Normal file
@ -0,0 +1,40 @@
|
||||
from karkas_piccolo.karkas import migrate_forward # isort:skip
|
||||
from karkas_piccolo.conf.apps import AppConfig, AppRegistry # isort:skip
|
||||
|
||||
from piccolo.conf.apps import PiccoloConfModule
|
||||
from piccolo.engine.sqlite import SQLiteEngine
|
||||
|
||||
from karkas_core.singleton import Singleton
|
||||
|
||||
# from karkas_piccolo.conf.apps import A
|
||||
|
||||
|
||||
APPS_CONFIGS = dict()
|
||||
|
||||
|
||||
def register_app_config(app_config: AppConfig):
|
||||
APPS_CONFIGS[app_config.app_name] = app_config
|
||||
|
||||
|
||||
def module_init():
|
||||
singleton = Singleton()
|
||||
singleton.storage["_database"] = dict()
|
||||
|
||||
pass
|
||||
|
||||
|
||||
async def module_late_init():
|
||||
singleton = Singleton()
|
||||
DB = SQLiteEngine(path="./db.sqlite3")
|
||||
|
||||
APP_REGISTRY = AppRegistry(apps_configs=APPS_CONFIGS)
|
||||
|
||||
module = PiccoloConfModule(name="standard.database")
|
||||
module.DB = DB
|
||||
module.APP_REGISTRY = APP_REGISTRY
|
||||
|
||||
singleton.storage["_database"]["conf"] = module
|
||||
|
||||
await migrate_forward()
|
||||
|
||||
pass
|
31
src/karkas_blocks/karkas_blocks/standard/filters/README.md
Normal file
31
src/karkas_blocks/karkas_blocks/standard/filters/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Блок Filters
|
||||
|
||||
Данный блок предоставляет фильтры для `aiogram`, которые используются для ограничения доступа к командам
|
||||
и обработчикам событий.
|
||||
|
||||
## Фильтры
|
||||
|
||||
| Фильтр | Описание |
|
||||
| :----------------------: | ---------------------------------------------------------------------- |
|
||||
| `ChatModerOrAdminFilter` | Пропускает сообщения только от модераторов и администраторов чата |
|
||||
| `ChatNotInApproveFilter` | Пропускает сообщения только из чатов, не входящих в список разрешенных |
|
||||
|
||||
## Использование
|
||||
|
||||
Фильтры можно использовать в декораторах `@router.message` и `@router.callback_query`.
|
||||
|
||||
## Пример
|
||||
|
||||
```python
|
||||
from aiogram import Router
|
||||
from karkas_core.modules_system.public_api import get_module
|
||||
|
||||
ChatModerOrAdminFilter = get_module("standard.filters", "ChatModerOrAdminFilter")
|
||||
|
||||
router = Router()
|
||||
|
||||
@router.message(ChatModerOrAdminFilter())
|
||||
async def admin_command(message: Message):
|
||||
# Обработка команды, доступной только администраторам и модераторам.
|
||||
pass
|
||||
```
|
@ -0,0 +1,8 @@
|
||||
from .filters import (
|
||||
ChatIDFilter,
|
||||
ChatModerOrAdminFilter,
|
||||
ChatNotInApproveFilter,
|
||||
SimpleAdminFilter,
|
||||
chat_not_in_approve,
|
||||
module_init,
|
||||
)
|
130
src/karkas_blocks/karkas_blocks/standard/filters/filters.py
Normal file
130
src/karkas_blocks/karkas_blocks/standard/filters/filters.py
Normal file
@ -0,0 +1,130 @@
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict
|
||||
|
||||
from aiogram import BaseMiddleware, Bot
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message, TelegramObject
|
||||
from aiogram.utils.chat_member import ADMINS
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from karkas_core.modules_system.public_api import (
|
||||
get_module,
|
||||
register_outer_message_middleware,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from karkas_blocks.standard.config import IConfig
|
||||
from karkas_blocks.standard.roles import Roles as IRoles
|
||||
|
||||
|
||||
config: "IConfig" = get_module("standard.config", "config")
|
||||
|
||||
try:
|
||||
Roles: "type[IRoles]" = get_module("standard.roles", "Roles")
|
||||
ROLES_MODULE_LOADED = True
|
||||
except Exception:
|
||||
ROLES_MODULE_LOADED = False
|
||||
pass
|
||||
|
||||
|
||||
class GlobalFilter(BaseMiddleware):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.filter = ChatIDFilter()
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
if not isinstance(event, Message):
|
||||
return await handler(event, data)
|
||||
|
||||
if not config.get("filters::global::enabled"):
|
||||
return await handler(event, data)
|
||||
|
||||
if await self.filter(event, None):
|
||||
return await handler(event, data)
|
||||
|
||||
if event.chat.type == "private":
|
||||
if config.get("filters::global::private_allowed"):
|
||||
return await handler(event, data)
|
||||
|
||||
return await event.answer(config.get("filters::global::forbidden_message"))
|
||||
|
||||
|
||||
def module_init():
|
||||
config.register(
|
||||
"filters::approved_chat_id", "int", multiple=True, shared=True, default_value=[]
|
||||
)
|
||||
config.register("filters::default_chat_tag", "string", shared=True)
|
||||
config.register(
|
||||
"filters::global::forbidden_message",
|
||||
"string",
|
||||
default_value="Вы не имеете доступа!",
|
||||
)
|
||||
config.register("filters::global::enabled", "boolean", default_value=False)
|
||||
config.register("filters::global::private_allowed", "boolean", default_value=False)
|
||||
|
||||
register_outer_message_middleware(GlobalFilter())
|
||||
|
||||
|
||||
def get_approved_chat_id() -> list:
|
||||
return config.get("filters::approved_chat_id")
|
||||
|
||||
|
||||
@deprecated("Use ChatIDFilter or own implementation")
|
||||
def chat_not_in_approve(message: Message) -> bool:
|
||||
chat_id = message.chat.id
|
||||
if chat_id in get_approved_chat_id():
|
||||
# log(f"Chat in approve list: {chat_id}")
|
||||
return False
|
||||
else:
|
||||
# log(f"Chat not in approve list: {chat_id}")
|
||||
return True
|
||||
|
||||
|
||||
class ChatIDFilter(BaseFilter):
|
||||
def __init__(self, blacklist=False, approved_chats=None) -> None:
|
||||
self.blacklist = blacklist
|
||||
self.approved_chats = approved_chats
|
||||
super().__init__()
|
||||
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
chat_id = message.chat.id
|
||||
|
||||
approved_chats = self.approved_chats or get_approved_chat_id()
|
||||
|
||||
# If filtering list is empty, allow anything
|
||||
if len(approved_chats) == 0:
|
||||
return True
|
||||
|
||||
res = chat_id in approved_chats
|
||||
|
||||
return res ^ (self.blacklist)
|
||||
|
||||
|
||||
class ChatNotInApproveFilter(ChatIDFilter):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(allow=False)
|
||||
|
||||
|
||||
class ChatModerOrAdminFilter(BaseFilter):
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
if not ROLES_MODULE_LOADED:
|
||||
raise Exception("Roles module not loaded")
|
||||
|
||||
user_id = message.from_user.id
|
||||
roles = Roles()
|
||||
admins = await bot.get_chat_administrators(message.chat.id)
|
||||
return (
|
||||
await roles.check_admin_permission(user_id)
|
||||
or await roles.check_moderator_permission(user_id)
|
||||
or any(user_id == admin.user.id for admin in admins)
|
||||
)
|
||||
|
||||
|
||||
class SimpleAdminFilter(BaseFilter):
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
member = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
return isinstance(member, ADMINS)
|
16
src/karkas_blocks/karkas_blocks/standard/filters/info.json
Normal file
16
src/karkas_blocks/karkas_blocks/standard/filters/info.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "standard.filters",
|
||||
"name": "Filters",
|
||||
"description": "Модуль с фильтрами",
|
||||
"author": "Karkas Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": true,
|
||||
"dependencies": {
|
||||
"required": {
|
||||
"standard.config": "^1.0.0"
|
||||
},
|
||||
"optional": {
|
||||
"standard.roles": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
# Блок FSM Database Storage
|
||||
|
||||
Данный блок реализует хранение состояний FSM (Finite State Machine) в базе данных.
|
||||
|
||||
## Функциональность
|
||||
|
||||
- Сохранение состояния FSM в базу данных;
|
||||
- Получение состояния FSM из базы данных;
|
||||
- Обновление данных состояния FSM.
|
||||
|
||||
## Использование
|
||||
|
||||
Блок автоматически регистрирует хранилище состояний FSM при инициализации.
|
@ -0,0 +1 @@
|
||||
from .fsm import module_init
|
@ -0,0 +1 @@
|
||||
from .piccolo_app import APP_CONFIG
|
@ -0,0 +1,15 @@
|
||||
import os
|
||||
|
||||
from karkas_piccolo.conf.apps import AppConfig
|
||||
|
||||
from .tables import FSMData
|
||||
|
||||
CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
APP_CONFIG = AppConfig(
|
||||
app_name="standard.fsm_database_storage",
|
||||
migrations_folder_path=os.path.join(CURRENT_DIRECTORY, "piccolo_migrations"),
|
||||
table_classes=[FSMData],
|
||||
migration_dependencies=[],
|
||||
commands=[],
|
||||
)
|
@ -0,0 +1,85 @@
|
||||
from piccolo.apps.migrations.auto.migration_manager import MigrationManager
|
||||
from piccolo.columns.column_types import Text
|
||||
from piccolo.columns.indexes import IndexMethod
|
||||
|
||||
ID = "2024-08-20T15:07:25:276545"
|
||||
VERSION = "1.16.0"
|
||||
DESCRIPTION = ""
|
||||
|
||||
|
||||
async def forwards():
|
||||
manager = MigrationManager(
|
||||
migration_id=ID,
|
||||
app_name="standard.fsm_database_storage",
|
||||
description=DESCRIPTION,
|
||||
)
|
||||
|
||||
manager.add_table(
|
||||
class_name="FSMData", tablename="fsm_data", schema=None, columns=None
|
||||
)
|
||||
|
||||
manager.add_column(
|
||||
table_class_name="FSMData",
|
||||
tablename="fsm_data",
|
||||
column_name="key",
|
||||
db_column_name="key",
|
||||
column_class_name="Text",
|
||||
column_class=Text,
|
||||
params={
|
||||
"primary": True,
|
||||
"default": "",
|
||||
"null": False,
|
||||
"primary_key": False,
|
||||
"unique": False,
|
||||
"index": False,
|
||||
"index_method": IndexMethod.btree,
|
||||
"choices": None,
|
||||
"db_column_name": None,
|
||||
"secret": False,
|
||||
},
|
||||
schema=None,
|
||||
)
|
||||
|
||||
manager.add_column(
|
||||
table_class_name="FSMData",
|
||||
tablename="fsm_data",
|
||||
column_name="state",
|
||||
db_column_name="state",
|
||||
column_class_name="Text",
|
||||
column_class=Text,
|
||||
params={
|
||||
"default": "",
|
||||
"null": True,
|
||||
"primary_key": False,
|
||||
"unique": False,
|
||||
"index": False,
|
||||
"index_method": IndexMethod.btree,
|
||||
"choices": None,
|
||||
"db_column_name": None,
|
||||
"secret": False,
|
||||
},
|
||||
schema=None,
|
||||
)
|
||||
|
||||
manager.add_column(
|
||||
table_class_name="FSMData",
|
||||
tablename="fsm_data",
|
||||
column_name="data",
|
||||
db_column_name="data",
|
||||
column_class_name="Text",
|
||||
column_class=Text,
|
||||
params={
|
||||
"default": "",
|
||||
"null": True,
|
||||
"primary_key": False,
|
||||
"unique": False,
|
||||
"index": False,
|
||||
"index_method": IndexMethod.btree,
|
||||
"choices": None,
|
||||
"db_column_name": None,
|
||||
"secret": False,
|
||||
},
|
||||
schema=None,
|
||||
)
|
||||
|
||||
return manager
|
@ -0,0 +1,8 @@
|
||||
from piccolo.columns import Text
|
||||
from piccolo.table import Table
|
||||
|
||||
|
||||
class FSMData(Table):
|
||||
key = Text(primary=True)
|
||||
state = Text(null=True)
|
||||
data = Text(null=True)
|
@ -0,0 +1,174 @@
|
||||
import json
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from aiogram.fsm.state import State
|
||||
from aiogram.fsm.storage.base import BaseStorage, StorageKey
|
||||
|
||||
from karkas_core.modules_system.public_api import get_module, log
|
||||
from karkas_core.modules_system.public_api.public_api import set_fsm
|
||||
|
||||
from .db.tables import FSMData
|
||||
|
||||
|
||||
class FSMDataRepository:
|
||||
def get(self, key: str):
|
||||
return FSMData.select().where(FSMData.key == key).first().run_sync()
|
||||
|
||||
def set_state(self, key: str, state: str):
|
||||
returning = (
|
||||
FSMData.update(
|
||||
{
|
||||
FSMData.state: state,
|
||||
}
|
||||
)
|
||||
.where(FSMData.key == key)
|
||||
.returning(FSMData.key)
|
||||
.run_sync()
|
||||
)
|
||||
|
||||
if len(returning) == 0:
|
||||
FSMData.insert(
|
||||
FSMData(
|
||||
key=key,
|
||||
state=state,
|
||||
data=None,
|
||||
)
|
||||
).run_sync()
|
||||
|
||||
def set_data(self, key: str, data: str):
|
||||
returning = (
|
||||
FSMData.update(
|
||||
{
|
||||
FSMData.data: data,
|
||||
}
|
||||
)
|
||||
.where(FSMData.key == key)
|
||||
.returning(FSMData.key)
|
||||
.run_sync()
|
||||
)
|
||||
|
||||
if len(returning) == 0:
|
||||
FSMData.insert(
|
||||
FSMData(
|
||||
key=key,
|
||||
data=data,
|
||||
state=None,
|
||||
)
|
||||
).run_sync()
|
||||
|
||||
|
||||
def serialize_key(key: StorageKey) -> str:
|
||||
return f"{key.bot_id}:{key.chat_id}:{key.user_id}"
|
||||
|
||||
|
||||
def serialize_object(obj: object) -> str | None:
|
||||
try:
|
||||
return json.dumps(obj)
|
||||
except Exception as e:
|
||||
log(f"Serializing error! {e}")
|
||||
return None
|
||||
|
||||
|
||||
def deserialize_object(obj):
|
||||
try:
|
||||
return json.loads(obj)
|
||||
except Exception as e:
|
||||
log(f"Deserializing error! {e}")
|
||||
return None
|
||||
|
||||
|
||||
class SQLStorage(BaseStorage):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.repo = FSMDataRepository()
|
||||
|
||||
async def set_state(self, key: StorageKey, state: State | None = None) -> None:
|
||||
"""
|
||||
Set state for specified key
|
||||
|
||||
:param key: storage key
|
||||
:param state: new state
|
||||
"""
|
||||
s_key = serialize_key(key)
|
||||
s_state = state.state if isinstance(state, State) else state
|
||||
|
||||
try:
|
||||
self.repo.set_state(s_key, s_state)
|
||||
except Exception as e:
|
||||
log(f"FSM Storage database error: {e}")
|
||||
|
||||
async def get_state(self, key: StorageKey) -> Optional[str]:
|
||||
"""
|
||||
Get key state
|
||||
|
||||
:param key: storage key
|
||||
:return: current state
|
||||
"""
|
||||
s_key = serialize_key(key)
|
||||
|
||||
try:
|
||||
s_state = self.repo.get(s_key)
|
||||
return s_state["state"] if s_state else None
|
||||
except Exception as e:
|
||||
log(f"FSM Storage database error: {e}")
|
||||
return None
|
||||
|
||||
async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Write data (replace)
|
||||
|
||||
:param key: storage key
|
||||
:param data: new data
|
||||
"""
|
||||
s_key = serialize_key(key)
|
||||
s_data = serialize_object(data)
|
||||
|
||||
try:
|
||||
self.repo.set_data(s_key, s_data)
|
||||
except Exception as e:
|
||||
log(f"FSM Storage database error: {e}")
|
||||
|
||||
async def get_data(self, key: StorageKey) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get current data for key
|
||||
|
||||
:param key: storage key
|
||||
:return: current data
|
||||
"""
|
||||
s_key = serialize_key(key)
|
||||
|
||||
try:
|
||||
s_data = self.repo.get(s_key)
|
||||
return deserialize_object(s_data["data"]) if s_data else None
|
||||
except Exception as e:
|
||||
log(f"FSM Storage database error: {e}")
|
||||
return None
|
||||
|
||||
async def update_data(
|
||||
self, key: StorageKey, data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update data in the storage for key (like dict.update)
|
||||
|
||||
:param key: storage key
|
||||
:param data: partial data
|
||||
:return: new data
|
||||
"""
|
||||
current_data = await self.get_data(key=key)
|
||||
if not current_data:
|
||||
current_data = {}
|
||||
current_data.update(data)
|
||||
await self.set_data(key=key, data=current_data)
|
||||
return current_data.copy()
|
||||
|
||||
async def close(self) -> None: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
async def module_init():
|
||||
register_app_config = get_module("standard.database", "register_app_config")
|
||||
from .db import APP_CONFIG
|
||||
|
||||
register_app_config(APP_CONFIG)
|
||||
|
||||
set_fsm(SQLStorage())
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "standard.fsm_database_storage",
|
||||
"name": "FSM Database Storage",
|
||||
"description": "Очень полезный модуль",
|
||||
"author": "Karkas Team",
|
||||
"version": "1.0.0",
|
||||
"privileged": false,
|
||||
"dependencies": {
|
||||
"required": {
|
||||
"standard.database": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
from .main import module_init
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user