Compare commits

...

103 Commits
0.1.1 ... main

Author SHA1 Message Date
4d25d2e83c
fix: add caption handle 2025-02-22 18:01:22 +03:00
7c25c7b4a9
fix: add tmp chat_member handler 2025-01-06 11:40:39 +03:00
e0a8bea938
style: fix lint errors 2024-12-29 23:59:24 +03:00
f97fcf4c34
fix: add version info in karkas_lite 2024-12-29 17:30:00 +03:00
3edee6e930
fix: disable standard.welcome in karkas_lite 2024-12-29 12:39:45 +03:00
3434230446
fix: rewrite Dockerfile 2024-10-10 02:12:33 +03:00
bd3a897557
ci: fix ci pipeline 2024-10-10 02:02:31 +03:00
3018bd217f Merged with fix/hotfix-spam 2024-10-10 01:41:05 +03:00
7b81191ad4
fix: add bot to delete_spam 2024-10-10 01:38:13 +03:00
9669118bf6
chore: remove altlinux 2024-10-10 01:37:55 +03:00
8e26f43587 Merged with fix/fix-spam-and-welcome 2024-10-10 00:57:49 +03:00
56b5a73f8d
fix: DO NOTHING on conflict and remove \n in welcome 2024-10-09 22:14:14 +03:00
c9afdf46ca Merged with feat/add-ai-spam-detection 2024-10-09 21:29:46 +03:00
b4eac59f80
feat: add spam detection 2024-10-08 19:10:54 +03:00
6c9e13b5db
feat: add standard.statistics to karkas_lite 2024-10-08 16:18:27 +03:00
b1d95a1496
feat: make save_messages optional in statistics 2024-10-08 16:18:08 +03:00
750b6be3a5 Merged with feat/add-spam-block 2024-10-07 15:38:50 +03:00
f64800949c
fix: change pop to remove 2024-10-07 12:13:01 +03:00
fe9bbebd79
ci: add karkas-lite build 2024-10-03 20:00:25 +03:00
9ac88652e0
feat: add karkas_lite 2024-10-03 19:59:23 +03:00
0881fe02d5
style: fix code formatting 2024-10-03 18:25:08 +03:00
7724a60f8c
feat: add spam block 2024-10-03 18:16:38 +03:00
x1z53
a6c0433569 style: refactor and translate comments 2024-08-29 23:22:04 +03:00
5950fa3bb8 Merged with style/add-flake8-type-checking 2024-08-26 20:18:12 +03:00
8f2adbfbc4
style: enable flake8-type-checking and move imports to TYPE_CHECKING 2024-08-26 17:01:56 +03:00
8baabcc413 Merged with chore/add-conventional-pre-commit 2024-08-26 16:05:34 +03:00
fab0b84a61
docs: correct step numbering in setup instructions 2024-08-26 15:57:39 +03:00
64a4066825
docs: add steps with pre-commit install 2024-08-26 15:53:19 +03:00
8ec93c89b7
chore: add conventional-pre-commit 2024-08-26 15:30:10 +03:00
Давид Султаниязов
6d421ee9b5 Merged with main 2024-08-26 11:26:57 +03:00
x1z53
5f5e851ecd Рефакторинг файлов README.md и документации 2024-08-25 14:52:07 +03:00
Armatik
04a4ab4868
backport help block update from KarkasLite 2024-08-22 22:56:07 +03:00
aab5fb4e39 Merged with experimental/redesign-db-block 2024-08-21 11:19:05 +03:00
5a76e94c41
вынес сообщение глобального фильтра в конфиг 2024-08-20 23:39:03 +03:00
ea7e12c5ed
добавляет проверку pyproject.toml и poetry.lock и исправляет сборку 2024-08-20 23:24:26 +03:00
7ebe631f9f
удаляет лишний блок 2024-08-20 22:57:06 +03:00
f5e15868f3
мелкие правки 2024-08-20 18:23:31 +03:00
9c9c93edf5
обновлен блок standard.filters 2024-08-20 18:19:21 +03:00
5513481330
удален блок standard.message_processing 2024-08-20 17:29:00 +03:00
8512b3300b
добавлен блок standard.chat 2024-08-20 17:26:09 +03:00
a3f0298288
добавлен блок standard.users 2024-08-20 17:08:51 +03:00
fa89265197
удален experimental.roles 2024-08-20 16:57:04 +03:00
7c196371c0
добавлен модуль со статистикой 2024-08-20 16:56:50 +03:00
a28e0b308f
мигрировал fsm_database_storage 2024-08-20 15:09:51 +03:00
9fb56ce1e0
wip 2024-08-20 13:55:48 +03:00
f5f662d6de
Squashed commit of the following:
commit 70890bbec6a4eedb47a75499a13de70fb8189137
Author: Maxim Slipenko <maxim@slipenko.com>
Date:   Tue Aug 20 11:49:18 2024 +0300

    wip: фиксит импорт

commit 1ee4c92ca0e4886b445397aa139d321919fd85da
Author: Maxim Slipenko <maxim@slipenko.com>
Date:   Tue Aug 20 11:48:55 2024 +0300

    wip: добавляет message_db_logger для проверки

commit a4c43fe5607e27103f03496f197292c4f942eb92
Author: Maxim Slipenko <maxim@slipenko.com>
Date:   Tue Aug 20 10:46:54 2024 +0300

    wip: piccolo test
2024-08-20 13:14:58 +03:00
c4f42bcf25
исправляет минимальную версию python 2024-08-19 15:44:45 +03:00
a7f1631869 Merged with backport-KarkasLite-into-main 2024-08-19 13:24:05 +03:00
8af22b76b2
исправляет копирование venv 2024-08-19 13:05:55 +03:00
707ce28182
выносит .venv в отдельный слой для оптимизации занимаемого места 2024-08-19 11:54:03 +03:00
827e6f6b44
меняет ocab на karkas в импортах 2024-08-19 11:30:13 +03:00
dc1abeec9f
исправлена сборка Гномика 2024-08-19 10:21:14 +03:00
1c1a8e1038
fix pre-commit 2024-08-19 10:09:31 +03:00
53065a3871
ci: добавил сборку Гномика 2024-08-19 09:58:48 +03:00
a25a97e8f7 подправил README.md Гномика 2024-08-18 22:59:10 +03:00
d987938ef9 Revert "Delete gnomik bot from OCAB Lite"
This reverts commit 984e4cf4e1.
2024-08-18 22:56:45 +03:00
21ae060c81 Revert "Delete external modules for OCAB Lite"
This reverts commit b185acd871.
2024-08-18 22:54:07 +03:00
b349a555eb Merge branch 'KarkasLite' into backport-KarkasLite-into-main 2024-08-18 22:50:41 +03:00
Armatik
3c0c8630eb
ci fix 2024-08-16 23:06:23 +03:00
Armatik
1fbe2b0c18
Переименование файлов и директорий 2024-08-16 22:53:33 +03:00
Armatik
2f634a4eef
Небольшие изменения в readme.md 2024-08-16 22:45:30 +03:00
Armatik
3c7dffc06d
Переименование проекта c OCAB в Karkas 2024-08-16 22:42:02 +03:00
Armatik
5b3963e87c
Добавил английскую версию со списком авторов проекта 2024-08-16 22:41:08 +03:00
Armatik
9fa5776fda
Переименовал файл со списком авторов 2024-08-16 22:40:33 +03:00
df1fed10c2 Merged with feat/add-standard-help 2024-08-16 16:25:42 +03:00
79298e5441 Merged with fix/add-limits-to-report 2024-08-16 16:23:57 +03:00
3514234526 Merged with cicd/init 2024-08-15 20:44:40 +03:00
913d84fb81 fix: исправление проблем линтеров 2024-08-15 19:33:04 +03:00
ac72ec7fa4 ci: тестирование работы CI 2024-08-15 19:26:47 +03:00
3f46a83de3 Merged with fix/welcome-stage-1 2024-08-14 00:08:25 +03:00
27a37b2f67 Merge branch 'OCAB-Lite' into fix/welcome-stage-1 2024-08-13 23:55:14 +03:00
2c06017cc3 добавлено отключение проверки на чаты, если они не указаны 2024-08-13 23:48:40 +03:00
c01dbcbe6b добавлена фильтрация по чатам 2024-08-13 23:37:34 +03:00
9ca32cfa28 отключил inline таски по дефолту 2024-08-13 23:16:31 +03:00
f6f0f8c02b изменил дефотный конфиг 2024-08-13 22:43:10 +03:00
58281f2580 фикс клавиатуры 2024-08-13 22:08:14 +03:00
b7cba315d7 фикс в BaseQuestionsTask 2024-08-13 22:03:36 +03:00
afbd277428 добавлено удаление старых сообщений об успехе 2024-08-13 21:54:58 +03:00
15cb6afb34 wip: Правки по standard.welcome 2024-08-13 21:24:40 +03:00
81ddb8509f wip: Правки по standard.welcome 2024-08-13 01:11:20 +03:00
Armatik
984e4cf4e1
Delete gnomik bot from OCAB Lite 2024-08-13 00:45:24 +03:00
Armatik
b185acd871
Delete external modules for OCAB Lite 2024-08-13 00:44:50 +03:00
Armatik
39500b77c2
delete legacy modules for OCAB Lite 2024-08-13 00:44:04 +03:00
Armatik
3076e1af13
Delete path to more module info files 2024-08-13 00:41:42 +03:00
Armatik
a9f6800518
Add info about ALTLinux OCAB Lite bot 2024-08-13 00:37:50 +03:00
Armatik
3ff4987bad
add contributors list to lite 2024-08-13 00:28:31 +03:00
Armatik
83bfcdddf7
add contributors list 2024-08-13 00:26:56 +03:00
73c1eb12e9 Merged with feat/add-welcome-module 2024-08-11 16:29:25 +03:00
5732b1bcc3 добавлен модуль welcome 2024-08-11 16:15:47 +03:00
e3443f835a Merged with feat/add-report-module 2024-08-11 11:10:29 +03:00
c746946d24 исправлен config-example.yaml 2024-08-11 09:50:03 +03:00
01912850e2 добавлен модуль standard.reports
- добавлен модуль standard.reports
- убрана обязательная зависимость в других модулях
- в config добавлен multiple
2024-08-11 09:47:38 +03:00
067fa52719 добавлен src/altlinux 2024-08-11 09:13:30 +03:00
e2f5e8daba Merged with feat/config-module 2024-08-02 16:59:51 +03:00
eecc59ca94 Merged with private/new-module-system 2024-07-14 17:07:46 +03:00
370b4fc648 Merged with private/new-module-system 2024-07-10 19:30:23 +03:00
2a2b9e15e8 Merged with chore/code-quality-tools 2024-07-08 14:20:21 +03:00
ef10f05a73 Добавлены nosec для прохождения bandit 2024-07-08 00:55:33 +03:00
ef0dda07f7 Добавлен bandit в pre-commit-hook 2024-07-08 00:49:04 +03:00
e80a01157f Автоматический рефакторинг и игнорирование flake8
Выполнен автоматический рефакторинг. Для тех файлов,
которые не прошли flake8 - был добавлен `noqa`, чтобы
в будущем исправить эти проблемы
2024-07-08 00:38:01 +03:00
4edeef4003 Добавлен pre-commit 2024-07-08 00:21:50 +03:00
31142dfb1c Добавлены инструменты для повышения качества кода 2024-07-07 23:59:33 +03:00
837613e072 Merged with chore/refactor-core 2024-07-07 21:25:10 +03:00
258 changed files with 20871 additions and 2109 deletions

1
.bandit Normal file
View File

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

8
.flake8 Normal file
View 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
View File

@ -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
View File

@ -0,0 +1,7 @@
armatik <s.fomchenkov@yandex.ru> <57626821+armatik@users.noreply.github.com>
armatik <s.fomchenkov@yandex.ru> <57626821+Armatik@users.noreply.github.com>
armatik <s.fomchenkov@yandex.ru> Armatik <s.fomchenkov@yandex.ru>
ilyazheprog <ilyazheprog@gmail.com> <ilya_zhenetskij@vk.com>
Maxim Slipenko <maxim@slipenko.com> Максим Слипенко <maxim@slipenko.com>

81
.pre-commit-config.yaml Normal file
View 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
View 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
View 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)

View File

@ -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
View 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
View 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
View 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

View File

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

15
init.py
View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

2
poetry.toml Normal file
View File

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

View File

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

View File

@ -1,4 +0,0 @@
#! /bin/sh
cd src
python -m unittest discover -v
cd ..

21
scripts/init.py Normal file
View File

@ -0,0 +1,21 @@
from json import dumps
from pathlib import Path
def main():
pwd = Path().cwd()
dir_core = pwd / "src" / "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
View 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
View File

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

View File

@ -1,2 +0,0 @@
import src.service
import src.core

View File

@ -1,13 +0,0 @@
TELEGRAM:
TOKEN:
YANDEXGPT:
TOKEN:
CATALOGID:
PROMPT:
ROLES:
ADMIN: 0
MODERATOR: 1
USER: 2
BOT: 3

View File

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

View File

@ -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()

View File

@ -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))

View File

@ -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
View 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"]

View 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
View File

@ -0,0 +1,60 @@
# Gnomик
![Логотип](./docs/gnomik.jpg)
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`.

View File

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

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

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

File diff suppressed because it is too large Load Diff

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

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

21
src/gnomik/pyproject.toml Normal file
View 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"

View File

@ -0,0 +1,10 @@
# Блоки Karkas
Блоки Karkas — это набор блоков для платформы «Каркас», которые добавляют функциональность ботам.
## Типы блоков
| Тип | Описание |
| :-------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Стандартные (`standard`) | Блоки, содержащие основной функционал: управление пользователями, ролями и настройками |
| Дополнительные (`external`) | Блоки, созданные командой разработки платформы «Каркас». Предоставляют расширенные возможности: интеграция с нейросетями, внешними сервисами и API |

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
{
"id": "external.create_report_apps",
"name": "Create Report Apps",
"description": "Модуль для создания отчетов о ошибках в приложениях",
"author": [
"OCAB Team",
"Maxim Slipenko"
],
"version": "1.0.0",
"privileged": false,
"dependencies": {
"standard.command_helper": "^1.0.0"
}
}

View File

@ -0,0 +1,115 @@
from typing import Union
from aiogram import Bot, F, Router
from aiogram.exceptions import TelegramForbiddenError
from aiogram.filters import BaseFilter, Command, CommandStart
from aiogram.types import (
CallbackQuery,
InlineKeyboardButton,
InlineKeyboardMarkup,
Message,
)
from 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", "Написать репорт о приложении")

View File

@ -0,0 +1,59 @@
import aiogram
class ReportFormatter:
def __init__(self, html=True):
self.html = html
def bold(self, string):
if self.html:
return f"<b>{self.text(string)}</b>"
return self.text(string)
def text(self, string):
if self.html:
return aiogram.html.quote(string)
return string
class Report:
def __init__(self, data: dict):
self.data = data
def export(self):
data = self.data
report = f"""
Стенд с ошибкой:
==============================
{data['system']}
Пакет:
==============================
{data['app']}
Шаги, приводящие к ошибке:
==============================
{data['problem_step_by_step']}
Фактический результат:
==============================
{data['actual']}
Ожидаемый результат:
==============================
{data['expected']}
"""
if data["additional"] != "":
report += f"""
Дополнительно:
==============================
{data['additional']}
"""
return report

View File

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

View File

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

View File

@ -0,0 +1,47 @@
# flake8: noqa
from typing import TYPE_CHECKING
from aiogram import Bot
from aiogram.types import Message
from 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")

View File

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

View File

@ -0,0 +1,50 @@
from typing import TYPE_CHECKING
from 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="помогите | не работает",
)

View 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("гномик")
)

View File

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

View 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}")

View File

@ -0,0 +1,26 @@
# Блок Admin
Данный блок предоставляет администраторам и модераторам чата инструменты для управления.
## Функциональность
- Удаление сообщений;
- Получение ID чата.
## Команды
| Команда | Описание |
| :-------: | --------------------------------------------------------- |
| `/rm` | Удаление сообщения, ответом на которое отправлена команда |
| `/chatID` | Получение ID текущего чата |
## Использование
Удаление сообщений:
1. Ответьте на сообщение, которое нужно удалить;
2. Отправьте команду `/rm`.
Получение ID чата:
1. Отправьте команду `/chatID`

View File

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

View 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
)

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

View 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)

View 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"))

View 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())

View File

@ -0,0 +1 @@
from .piccolo_app import APP_CONFIG

View File

@ -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=[],
)

View File

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

View File

@ -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()

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

View 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

View File

@ -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")
```

View File

@ -0,0 +1 @@
from .main import get_user_commands, module_late_init, register_command

View File

@ -0,0 +1,9 @@
{
"id": "standard.command_helper",
"name": "Command helper",
"description": "Модуль для отображения команд при вводе '/'",
"author": "Karkas Team",
"version": "1.0.0",
"privileged": false,
"dependencies": {}
}

View File

@ -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()

View 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")
```

View File

@ -0,0 +1,2 @@
from .config import IConfig, config
from .main import module_late_init

View File

@ -0,0 +1,7 @@
# flake8: noqa
from .config_manager import ConfigManager
IConfig = ConfigManager
config: ConfigManager = ConfigManager(config_path="config.yaml")

View File

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

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

View 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

View 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

View File

@ -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()

View File

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

View File

@ -0,0 +1,9 @@
{
"id": "standard.database",
"name": "Database",
"description": "Модуль для работы с БД",
"author": "Karkas Team",
"version": "1.0.0",
"privileged": true,
"dependencies": {}
}

View 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

View 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
```

View File

@ -0,0 +1,8 @@
from .filters import (
ChatIDFilter,
ChatModerOrAdminFilter,
ChatNotInApproveFilter,
SimpleAdminFilter,
chat_not_in_approve,
module_init,
)

View 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)

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

View File

@ -0,0 +1,13 @@
# Блок FSM Database Storage
Данный блок реализует хранение состояний FSM (Finite State Machine) в базе данных.
## Функциональность
- Сохранение состояния FSM в базу данных;
- Получение состояния FSM из базы данных;
- Обновление данных состояния FSM.
## Использование
Блок автоматически регистрирует хранилище состояний FSM при инициализации.

View File

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

View File

@ -0,0 +1 @@
from .piccolo_app import APP_CONFIG

View File

@ -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=[],
)

View File

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

View File

@ -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)

View File

@ -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())

View File

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

View File

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

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