Compare commits

..

No commits in common. "main" and "0.1.1" have entirely different histories.
main ... 0.1.1

258 changed files with 2106 additions and 20868 deletions

View File

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

View File

@ -1,8 +0,0 @@
[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,7 +5,6 @@ env
.venv
venv
__pycache__
Karkas.db
config.yaml
dist
*.py[cod]
OCAB.db
src/paths.json
src/core/config.yaml

View File

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

View File

@ -1,81 +0,0 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/crashappsec/pre-commit-sync
rev: 04b0e02eefa7c41bedca7456ad542e60b67c16c6
hooks:
- id: pre-commit-sync
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/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: []

View File

@ -1,8 +0,0 @@
Руководитель проекта:
- Семен Фомченков (@Armatik), e-mail: armatik@alt-gnome.ru
Ведущие разработчики:
- Максим Слипенко (@Maks1m_S), e-mail: maxim@slipenko.com
Участники проекта:
- Илья Женецкий (@ilyazheprog)

View File

@ -1,8 +0,0 @@
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,45 +1,58 @@
# Каркас
# OpenChatAiBot V2
## Что такое «Каркас»?
## Что такое OCAB?
Каркас — это платформа для разработки блочных Telegram-ботов, которая призвана упростить взаимодействие с чатами. «Каркас» предоставляет возможность расширять функциональность бота с помощью интеграции различных блоков. Код платформы и набор стандартных блоков находятся в этом монорепозитории.
OCAB - это бот для Telegram, который призван помочь во взаимодействии с чатом.
Бот поддерживает интеграцию модулей для расширения функционала.
Фактически бот является платформой для запуска созданных для него модулей.
Модули могут взаимодействовать друг с другом или быть полностью независимыми.
## Структура монорепозитория
## Что такое модуль?
Монорепозиторий Karkas включает в себя:
Модуль - это директория, которая содержит в себе код модуля и его конфигурацию.
- **Ядро Karkas (`src/karkas_core`):** Основные компоненты платформы, такие как система управления блоками, логирование и утилиты.
- **Блоки Karkas (`src/karkas_blocks`):** Содержит стандартные и дополнительные блоки, которые расширяют функциональность ботов, созданных на платформе «Каркас».
- **Бот Gnomик (`src/gnomik`):** Пример реализации бота, созданного на основе платформы «Каркас».
### Структура модуля
## Блоки
*Будет дополнено после закрытия [issue #17](https://gitflic.ru/project/armatik/ocab/issue/17).*
Блоки Karkas — это независимые компоненты, которые добавляют функциональность бота.
## Стандартные модули
### Структура блока
В стандартный состав бота входят следующие модули:
Структура блока представлена [здесь](docs/BLOCKS-SPEC.md).
* `admin` - модуль для модерирования чата. Позволяет удалять сообщения, банить пользователей и т.д.
* `reputation` - модуль репутации пользователей. Позволяет оценивать ответы пользователей и накапливать репутацию.
* `welcome` - модуль приветствия новых пользователей. Позволяет приветствовать новых пользователей в чате, а также
проверять пользователя капчей для предотвращения спама.
* `roles` - модуль ролей. Позволяет назначать пользователям роли и ограничивать доступ к командам бота по ролям.
Является важной частью системы прав доступа и модуля `admin`.
### Стандартные блоки
## Дополнительные официальные модули
Стандартные блоки предоставляют базовые функции для работы бота
* `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
Дополнительные официальные блоки созданы командой разработки платформы «Каркас» и предоставляют расширенные возможности для бота:
### Вручную
- [`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) — блок для создания отчётов об ошибках.
## Технологический стек
* 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 для работы с базой данных.

View File

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

View File

@ -1,42 +0,0 @@
# Настройка рабочего окружения
Данная инструкция поможет вам настроить рабочее окружение для разработки 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>` — имя пакета) для добавления новых зависимостей.

View File

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

47
how-to install deps.md Normal file
View File

@ -0,0 +1,47 @@
## 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 Normal file
View File

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

View File

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

1046
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,48 +1,24 @@
[tool.poetry]
name = "karkas-monorepo"
version = "2.0.0"
description = "Karkas is a modular Telegram bot"
name = "ocab"
version = "0.1.0"
description = ""
license = "GPL-3.0-only"
authors = ["Семён Фомченков <s.fomchenkov@yandex.ru>"]
maintainers = [
"Илья Женецкий <ilya_zhenetskij@vk.com>",
"qualimock <qualimock@yandex.ru>",
"Кирилл Уницаев <fiersik.kouji@yandex.ru>",
"Максим Слипенко <maxim@slipenko.com>"
"Кирилл Уницаев fiersik.kouji@yandex.ru",
]
readme = "README.md"
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'
repository = "https://gitflic.ru/project/armatik/ocab"
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
python = "^3.11.6"
aiogram = "^3.2.0"
peewee = "^3.17.0"
pyyaml = "^6.0.1"
requests = "^2.31.0"
[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"]

4
run_tests Normal file
View File

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

View File

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

View File

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

View File

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

2
src/__init__.py Normal file
View File

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

13
src/core/config.yaml Normal file
View File

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

View File

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

24
src/core/logger.py Normal file
View File

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

26
src/core/main.py Normal file
View File

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

18
src/core/routers.py Normal file
View File

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

View File

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

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

View File

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

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -1,115 +0,0 @@
from typing import Union
from aiogram import Bot, F, Router
from aiogram.exceptions import TelegramForbiddenError
from aiogram.filters import BaseFilter, Command, CommandStart
from aiogram.types import (
CallbackQuery,
InlineKeyboardButton,
InlineKeyboardMarkup,
Message,
)
from 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

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

View File

@ -1,24 +0,0 @@
# Блок 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

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

View File

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

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

View File

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

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

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

View File

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

View File

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

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

@ -1,7 +0,0 @@
from karkas_core.modules_system.public_api import register_router
from .routers import router
async def module_init():
register_router(router)

View File

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

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

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

View File

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

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

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

@ -1,13 +0,0 @@
{
"id": "standard.chats",
"name": "Чаты",
"description": "Очень полезный модуль",
"author": "Karkas Team",
"version": "1.0.0",
"privileged": true,
"dependencies": {
"required": {
"standard.database": "^1.0.0"
}
}
}

View File

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

@ -1,23 +0,0 @@
# Блок 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

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

View File

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

View File

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

@ -1,28 +0,0 @@
# Блок 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

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

View File

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

View File

@ -1,115 +0,0 @@
import inspect
from typing import Any, Dict, List
import yaml
class ConfigManager:
def __init__(self, config_path: str):
self.config_path = config_path
self._config: Dict[str, Dict[str, Any]] = {}
self._metadata: Dict[str, Dict[str, Any]] = {}
def load(self, file_path: str = ""):
if not file_path:
file_path = self.config_path
def build_key(prev, next):
if prev:
return f"{prev}::{next}"
return next
def recurse_set(value, key=""):
if isinstance(value, dict):
for k, v in value.items():
recurse_set(v, build_key(key, k))
return
if key in self._metadata:
self._config[key] = value
with open(file_path, "r", encoding="utf-8") as file:
data = yaml.safe_load(file)
recurse_set(data)
def save(self, file_path: str = ""):
if not file_path:
file_path = self.config_path
def nested_dict(flat_dict):
result = {}
for key, value in flat_dict.items():
keys = key.split("::")
d = result
for k in keys[:-1]:
d = d.setdefault(k, {})
d[keys[-1]] = value
return result
with open(file_path, "w", encoding="utf-8") as file:
yaml.dump(nested_dict(self._config), file, allow_unicode=True)
def _check_rights(self, key, module_id, access_type="get"):
return
def get(self, key: str):
module_id = self._get_module_id()
self._check_rights(key, module_id)
return self._config.get(key, self._metadata.get(key).get("default_value"))
def get_meta(self, key: str):
module_id = self._get_module_id()
self._check_rights(key, module_id, "get_meta")
return self._metadata.get(key)
def _get_module_id(self):
caller_frame = inspect.currentframe().f_back.f_back
caller_globals = caller_frame.f_globals
module_id = caller_globals.get("__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

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

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

@ -1,267 +0,0 @@
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 +0,0 @@
from .main import module_init, module_late_init, register_app_config

View File

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

View File

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

@ -1,31 +0,0 @@
# Блок 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

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

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

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

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

View File

@ -1,21 +0,0 @@
{
"id": "standard.help",
"name": "Help",
"description": "Модуль для вывода /help сообщения",
"author": "Karkas Team",
"version": "1.0.1",
"privileged": false,
"dependencies": {
"required": {
"standard.config": "^1.0.0"
},
"optional": {
"standard.command_helper": "^1.0.0"
}
},
"pythonDependencies": {
"required": {
"string": "*"
}
}
}

View File

@ -1,81 +0,0 @@
import string
from typing import TYPE_CHECKING
from aiogram import Router
from aiogram.filters import Command
from karkas_core.modules_system.public_api import (
get_metainfo,
get_module,
register_router,
)
if TYPE_CHECKING:
from aiogram.types import Message
from karkas_blocks.standard.config import IConfig
config: "IConfig" = get_module("standard.config", "config")
try:
(register_command, get_user_commands) = get_module(
"standard.command_helper", ["register_command", "get_user_commands"]
)
COMMAND_HELPER_MODULE_LOADED = True
except Exception:
COMMAND_HELPER_MODULE_LOADED = False
pass
# We kindly ask you not to delete or change the following text without
# the consent of the «Karkas» project team.
FOOTER = """===============
Разработано командой ALT Gnome Infrastructure в рамках проекта Каркас.
Исходный код: https://gitflic.ru/project/alt-gnome/karkas
Оставить репорт: https://gitflic.ru/project/alt-gnome/karkas/issue/create
Руководитель проекта: Семен Фомченков
Ведущий разработчик: Максим Слипенко
Почта для связи: help@karkas.chat
Версия: $version
"""
def format_commands(commands_dict):
formatted_commands = []
for command, details in commands_dict.items():
formatted_commands.append(f"/{command} - {details['long_description']}")
return "\n".join(formatted_commands)
async def help(message: "Message"):
commands = ""
version = ""
if COMMAND_HELPER_MODULE_LOADED:
commands = format_commands(get_user_commands())
metainfo = get_metainfo()
if "app_version" in metainfo:
version = metainfo["app_version"]
await message.reply(
string.Template(config.get("help::message") + "\n\n" + FOOTER).substitute(
commands=commands, version=version or "не указана"
)
)
async def module_init():
config.register(
"help::message",
"string",
default_value="$commands",
)
router = Router()
router.message.register(help, Command("help"))
register_router(router)
if COMMAND_HELPER_MODULE_LOADED:
register_command("help", "Cправка")

View File

@ -1,20 +0,0 @@
# Блок Info
Данный блок предоставляет информацию о пользователях и чатах.
## Команды
| Команда | Описание |
| :---------: | ----------------------------------- |
| `/info` | Получение информации о пользователе |
| `/chatinfo` | Получение информации о чате |
## Использование
Информация о пользователе:
1. Отправьте команду `/info`, ответив на сообщение пользователя или указав его тег.
Информация о чате:
1. Отправьте команду `/chatinfo`.

View File

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

View File

@ -1,54 +0,0 @@
# 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
if TYPE_CHECKING:
from karkas_blocks.standard.database import db_api as IDbApi
from karkas_blocks.standard.roles import Roles as IRoles
db_api: "IDbApi" = get_module(
"standard.database",
"db_api",
)
Roles: "type[IRoles]" = get_module("standard.roles", "Roles")
async def get_info_answer_by_id(message: Message, bot: Bot, user_id: int):
roles = Roles()
role = roles.get_user_role(user_id)
answer = f"Имя: {user_id}\n" f"Роль: {role}\n"
await message.reply(answer)
async def get_user_info(message: Message, bot: Bot):
try:
if message.reply_to_message:
user_id = message.reply_to_message.from_user.id
await get_info_answer_by_id(message, bot, user_id)
else:
await get_info_answer_by_id(message, bot, message.from_user.id)
except Exception as e:
await message.reply(
"В вашем запросе что-то пошло не так,"
" попробуйте запросить информацию о пользователе по его тегу или ответив на его сообщение"
)
# print(e)
log(e)
async def get_chat_info(message: Message, bot: Bot):
answer = (
f"*Название чата:* {message.chat.title}\n"
f"*ID чата:* `{message.chat.id}`\n \n"
f"*Суммарное количество сообщений в чате:* {db_api.get_chat_all_stat(message.chat.id)}\n"
f"*Количество пользователей в чате:* {await bot.get_chat_member_count(message.chat.id)}\n"
f"*Количество администраторов в чате:* {len(await bot.get_chat_administrators(message.chat.id))}"
)
await message.reply(answer, parse_mode="MarkdownV2")

View File

@ -1,15 +0,0 @@
{
"id": "standard.info",
"name": "Info",
"description": "Модуль с информацией",
"author": "Karkas Team",
"version": "1.0.0",
"privileged": false,
"dependencies": {
"required": {
"standard.roles": "^1.0.0",
"standard.database": "^1.0.0",
"standard.command_helper": "^1.0.0"
}
}
}

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