matele 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matele-0.1.0/.editorconfig +35 -0
- matele-0.1.0/.gitattributes +1 -0
- matele-0.1.0/.github/dependabot.yml +7 -0
- matele-0.1.0/.github/workflows/ci.yml +38 -0
- matele-0.1.0/.github/workflows/docs.yml +42 -0
- matele-0.1.0/.github/workflows/release.yml +40 -0
- matele-0.1.0/.gitignore +12 -0
- matele-0.1.0/CHANGELOG.md +28 -0
- matele-0.1.0/LICENSE +21 -0
- matele-0.1.0/PKG-INFO +310 -0
- matele-0.1.0/README.md +267 -0
- matele-0.1.0/RELEASING.md +19 -0
- matele-0.1.0/ROADMAP.md +35 -0
- matele-0.1.0/docs/broadcasts.md +224 -0
- matele-0.1.0/docs/buttons.md +229 -0
- matele-0.1.0/docs/changelog.md +3 -0
- matele-0.1.0/docs/concepts.md +99 -0
- matele-0.1.0/docs/deploy.md +225 -0
- matele-0.1.0/docs/errors.md +144 -0
- matele-0.1.0/docs/files.md +215 -0
- matele-0.1.0/docs/filters.md +205 -0
- matele-0.1.0/docs/first-bot.md +171 -0
- matele-0.1.0/docs/groups.md +151 -0
- matele-0.1.0/docs/handlers.md +211 -0
- matele-0.1.0/docs/index.md +89 -0
- matele-0.1.0/docs/menus.md +129 -0
- matele-0.1.0/docs/mini-apps.md +190 -0
- matele-0.1.0/docs/platform-api.md +175 -0
- matele-0.1.0/docs/reference.md +225 -0
- matele-0.1.0/docs/routers.md +183 -0
- matele-0.1.0/docs/states.md +232 -0
- matele-0.1.0/docs/stylesheets/extra.css +3 -0
- matele-0.1.0/docs/testing.md +241 -0
- matele-0.1.0/docs/webhooks.md +228 -0
- matele-0.1.0/examples/mini_app/app.py +82 -0
- matele-0.1.0/examples/mini_app/index.html +54 -0
- matele-0.1.0/examples/two_messengers.py +98 -0
- matele-0.1.0/mkdocs.yml +77 -0
- matele-0.1.0/pyproject.toml +77 -0
- matele-0.1.0/schema/max/README.md +20 -0
- matele-0.1.0/schema/max/schema.json +5388 -0
- matele-0.1.0/src/matele/__init__.py +96 -0
- matele-0.1.0/src/matele/_http.py +39 -0
- matele-0.1.0/src/matele/callbacks.py +225 -0
- matele-0.1.0/src/matele/dispatch/__init__.py +19 -0
- matele-0.1.0/src/matele/dispatch/context.py +226 -0
- matele-0.1.0/src/matele/dispatch/dispatcher.py +400 -0
- matele-0.1.0/src/matele/dispatch/filters.py +167 -0
- matele-0.1.0/src/matele/dispatch/panel.py +119 -0
- matele-0.1.0/src/matele/dispatch/polling.py +151 -0
- matele-0.1.0/src/matele/dispatch/router.py +190 -0
- matele-0.1.0/src/matele/errors.py +103 -0
- matele-0.1.0/src/matele/fsm/__init__.py +20 -0
- matele-0.1.0/src/matele/fsm/context.py +54 -0
- matele-0.1.0/src/matele/fsm/redis.py +114 -0
- matele-0.1.0/src/matele/fsm/sqlite.py +130 -0
- matele-0.1.0/src/matele/fsm/state.py +93 -0
- matele-0.1.0/src/matele/fsm/storage.py +76 -0
- matele-0.1.0/src/matele/hub.py +217 -0
- matele-0.1.0/src/matele/max/__init__.py +7 -0
- matele-0.1.0/src/matele/max/_model.py +122 -0
- matele-0.1.0/src/matele/max/api.py +516 -0
- matele-0.1.0/src/matele/max/bot.py +460 -0
- matele-0.1.0/src/matele/max/certs/README.md +6 -0
- matele-0.1.0/src/matele/max/certs/russian_trusted_root_ca.pem +33 -0
- matele-0.1.0/src/matele/max/models.py +1539 -0
- matele-0.1.0/src/matele/py.typed +0 -0
- matele-0.1.0/src/matele/telegram/__init__.py +6 -0
- matele-0.1.0/src/matele/telegram/api.py +255 -0
- matele-0.1.0/src/matele/telegram/bot.py +628 -0
- matele-0.1.0/src/matele/testing.py +289 -0
- matele-0.1.0/src/matele/types.py +253 -0
- matele-0.1.0/src/matele/webapp.py +127 -0
- matele-0.1.0/src/matele/webhook.py +267 -0
- matele-0.1.0/tests/__init__.py +0 -0
- matele-0.1.0/tests/max_fake.py +100 -0
- matele-0.1.0/tests/test_callbacks.py +138 -0
- matele-0.1.0/tests/test_commands_menu.py +100 -0
- matele-0.1.0/tests/test_dispatch.py +605 -0
- matele-0.1.0/tests/test_docs.py +45 -0
- matele-0.1.0/tests/test_errors.py +70 -0
- matele-0.1.0/tests/test_examples.py +50 -0
- matele-0.1.0/tests/test_fsm.py +78 -0
- matele-0.1.0/tests/test_groups.py +245 -0
- matele-0.1.0/tests/test_hub.py +151 -0
- matele-0.1.0/tests/test_max_api.py +262 -0
- matele-0.1.0/tests/test_max_bot.py +271 -0
- matele-0.1.0/tests/test_max_models.py +87 -0
- matele-0.1.0/tests/test_media.py +132 -0
- matele-0.1.0/tests/test_panel.py +125 -0
- matele-0.1.0/tests/test_review_fixes.py +296 -0
- matele-0.1.0/tests/test_storages.py +186 -0
- matele-0.1.0/tests/test_telegram.py +379 -0
- matele-0.1.0/tests/test_webapp.py +135 -0
- matele-0.1.0/tests/test_webhook.py +268 -0
- matele-0.1.0/tools/gen_max_models.py +231 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# https://editorconfig.org — отступы табами во всём коде проекта.
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
[*]
|
|
5
|
+
charset = utf-8
|
|
6
|
+
end_of_line = lf
|
|
7
|
+
insert_final_newline = true
|
|
8
|
+
trim_trailing_whitespace = true
|
|
9
|
+
indent_style = tab
|
|
10
|
+
indent_size = tab
|
|
11
|
+
tab_width = 4
|
|
12
|
+
|
|
13
|
+
# Фронтенд исторически с шириной отступа 2.
|
|
14
|
+
[*.{js,css,html,json}]
|
|
15
|
+
tab_width = 2
|
|
16
|
+
|
|
17
|
+
# YAML запрещает табы в отступах (спецификация), поэтому здесь пробелы.
|
|
18
|
+
[*.{yml,yaml}]
|
|
19
|
+
indent_style = space
|
|
20
|
+
indent_size = 2
|
|
21
|
+
|
|
22
|
+
# В Markdown таб означает блок кода, а два пробела в конце строки — перенос.
|
|
23
|
+
[*.md]
|
|
24
|
+
indent_style = space
|
|
25
|
+
indent_size = 2
|
|
26
|
+
trim_trailing_whitespace = false
|
|
27
|
+
|
|
28
|
+
# Бинарные и вендорные файлы не трогаем.
|
|
29
|
+
[*.{pem,db,png,ico}]
|
|
30
|
+
charset = unset
|
|
31
|
+
end_of_line = unset
|
|
32
|
+
insert_final_newline = unset
|
|
33
|
+
trim_trailing_whitespace = unset
|
|
34
|
+
indent_style = unset
|
|
35
|
+
indent_size = unset
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ${{ matrix.os }}
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
os: [ubuntu-latest]
|
|
14
|
+
python: ["3.11", "3.12", "3.13"]
|
|
15
|
+
include:
|
|
16
|
+
- os: windows-latest
|
|
17
|
+
python: "3.13"
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
20
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python }}
|
|
23
|
+
cache: pip
|
|
24
|
+
- run: pip install -e ".[dev]"
|
|
25
|
+
- name: Lint
|
|
26
|
+
run: ruff check src tests tools examples
|
|
27
|
+
- name: Formatting (tabs, see .editorconfig)
|
|
28
|
+
run: ruff format --check src tests tools examples
|
|
29
|
+
- name: Types
|
|
30
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.12'
|
|
31
|
+
run: basedpyright --pythonpath "$(which python)"
|
|
32
|
+
- name: Tests
|
|
33
|
+
run: python -m pytest -q --cov=matele --cov-report=term-missing:skip-covered --cov-fail-under=90
|
|
34
|
+
- name: Generated MAX models match the schema
|
|
35
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.12'
|
|
36
|
+
run: |
|
|
37
|
+
python tools/gen_max_models.py
|
|
38
|
+
git diff --exit-code src/matele/max/models.py
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Builds the documentation site on every push and publishes it to GitHub
|
|
2
|
+
# Pages from main.
|
|
3
|
+
name: Docs
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
14
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
cache: pip
|
|
18
|
+
- run: pip install ".[docs]"
|
|
19
|
+
- run: mkdocs build --strict
|
|
20
|
+
env:
|
|
21
|
+
NO_MKDOCS_2_WARNING: "1"
|
|
22
|
+
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
|
23
|
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
24
|
+
with:
|
|
25
|
+
path: site
|
|
26
|
+
|
|
27
|
+
deploy:
|
|
28
|
+
needs: build
|
|
29
|
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
permissions:
|
|
32
|
+
pages: write
|
|
33
|
+
id-token: write
|
|
34
|
+
environment:
|
|
35
|
+
name: github-pages
|
|
36
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
37
|
+
concurrency:
|
|
38
|
+
group: pages
|
|
39
|
+
cancel-in-progress: false
|
|
40
|
+
steps:
|
|
41
|
+
- id: deployment
|
|
42
|
+
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Publishes to PyPI when a version tag (v0.1.0, ...) is pushed.
|
|
2
|
+
# Uses PyPI Trusted Publishing: no token is stored in the repository.
|
|
3
|
+
name: Release
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
14
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
- run: pip install build twine
|
|
18
|
+
- name: The tag matches the package version
|
|
19
|
+
run: |
|
|
20
|
+
version=$(python -c "import re; print(re.search(r'__version__ = \"(.+)\"', open('src/matele/__init__.py').read()).group(1))")
|
|
21
|
+
test "v$version" = "$GITHUB_REF_NAME" || { echo "tag $GITHUB_REF_NAME != version $version"; exit 1; }
|
|
22
|
+
- run: python -m build
|
|
23
|
+
- run: twine check --strict dist/*
|
|
24
|
+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
25
|
+
with:
|
|
26
|
+
name: dist
|
|
27
|
+
path: dist/
|
|
28
|
+
|
|
29
|
+
publish:
|
|
30
|
+
needs: build
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
environment: pypi
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1
|
matele-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Изменения
|
|
2
|
+
|
|
3
|
+
Версии нумеруются как `MAJOR.MINOR.PATCH`. До 1.0 ломающие изменения возможны в любой версии `0.x`, но каждое такое изменение записывается здесь в разделе «Ломающие изменения» вместе с тем, что нужно поправить в своём коде. Исправления без изменения API выходят как `PATCH`.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 (2026-11-10)
|
|
6
|
+
|
|
7
|
+
Первая версия. Что в ней есть:
|
|
8
|
+
|
|
9
|
+
- общий слой для MAX и Telegram: сообщения, кнопки, файлы, ответы на сообщения, статус «печатает…»
|
|
10
|
+
- роутеры, фильтры, middleware, состояния с хранилищами в памяти, SQLite и Redis
|
|
11
|
+
- типизированные данные кнопок
|
|
12
|
+
- групповые чаты и события входа и выхода участников
|
|
13
|
+
- меню в одном сообщении (`ctx.show()`)
|
|
14
|
+
- меню команд из `@dp.command(..., description=...)`
|
|
15
|
+
- мини-приложения: кнопка открытия и проверка `initData`
|
|
16
|
+
- рассылки во все мессенджеры человека (`Hub`)
|
|
17
|
+
- long polling и вебхуки (ASGI)
|
|
18
|
+
- весь MAX Bot API с моделями из официальной схемы, `TelegramApi.call()` для любого метода Telegram
|
|
19
|
+
- `FakeBot` для тестов без сети
|
|
20
|
+
- `escape()` для текста, который пришёл снаружи: сообщения уходят как HTML
|
|
21
|
+
- подпись данных кнопок (`callback_secret`), чтобы бот принимал только свои кнопки
|
|
22
|
+
- доставка «минимум один раз» для polling и вебхуков (`at_least_once=True`)
|
|
23
|
+
- ограничение числа одновременно обрабатываемых событий (`concurrency`, `max_pending`)
|
|
24
|
+
- остановка по Ctrl+C и SIGTERM с ожиданием начатых хендлеров
|
|
25
|
+
- блокировка разговора между процессами в `RedisStorage`
|
|
26
|
+
- продолжение рассылки после перезапуска (`on_sent`)
|
|
27
|
+
- лимит на размер скачиваемых файлов (`max_download`, по умолчанию 50 МБ)
|
|
28
|
+
- документация на сайте https://cvsqwe.github.io/matele/
|
matele-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cvsqwe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
matele-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: matele
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One bot, two messengers: an async framework for MAX and Telegram bots
|
|
5
|
+
Project-URL: Homepage, https://github.com/cvsqwe/matele
|
|
6
|
+
Project-URL: Documentation, https://cvsqwe.github.io/matele/
|
|
7
|
+
Project-URL: Source, https://github.com/cvsqwe/matele
|
|
8
|
+
Project-URL: Issues, https://github.com/cvsqwe/matele/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/cvsqwe/matele/blob/main/CHANGELOG.md
|
|
10
|
+
Author: cvsqwe
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: asyncio,bot,framework,max,telegram
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Natural Language :: Russian
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Communications :: Chat
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: certifi>=2024.2.2
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: basedpyright==1.40.2; extra == 'dev'
|
|
29
|
+
Requires-Dist: fastapi>=0.110; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
33
|
+
Requires-Dist: redis>=5; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.17.4; extra == 'dev'
|
|
35
|
+
Requires-Dist: uvicorn>=0.29; extra == 'dev'
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: mkdocs-material<10,>=9.6; extra == 'docs'
|
|
38
|
+
Provides-Extra: redis
|
|
39
|
+
Requires-Dist: redis>=5; extra == 'redis'
|
|
40
|
+
Provides-Extra: webhook
|
|
41
|
+
Requires-Dist: uvicorn>=0.29; extra == 'webhook'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# matele
|
|
45
|
+
|
|
46
|
+
Фреймворк на Python для ботов, которые работают сразу в MAX и Telegram. Хендлеры пишутся один раз, а разницу между мессенджерами matele берёт на себя.
|
|
47
|
+
|
|
48
|
+
Проект на ранней стадии: до версии 1.0 API ещё может меняться. Все изменения записываются в [CHANGELOG.md](https://github.com/cvsqwe/matele/blob/main/CHANGELOG.md).
|
|
49
|
+
|
|
50
|
+
Документация с руководством: https://cvsqwe.github.io/matele/
|
|
51
|
+
|
|
52
|
+
## Установка
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install matele
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Нужен Python 3.11 или новее. Для вебхуков добавьте `[webhook]`, для хранения состояния в Redis `[redis]`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install "matele[webhook,redis]"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Пример
|
|
65
|
+
|
|
66
|
+
Бот, который здоровается, спрашивает имя и запоминает его. Один и тот же код работает в обоих мессенджерах:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from matele import Button, CallbackData, Context, Dispatcher, Keyboard, State, callback, escape
|
|
70
|
+
from matele.max import MaxBot
|
|
71
|
+
from matele.telegram import TelegramBot
|
|
72
|
+
|
|
73
|
+
dp = Dispatcher()
|
|
74
|
+
ASK_NAME = State("ask_name")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@callback("hello")
|
|
78
|
+
class Hello(CallbackData):
|
|
79
|
+
lang: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dp.command("start")
|
|
83
|
+
async def start(ctx: Context):
|
|
84
|
+
keyboard = Keyboard([[Button("Русский", Hello("ru").pack()), Button("English", Hello("en").pack())]])
|
|
85
|
+
await ctx.reply("Привет! На каком языке говорим?", keyboard)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dp.callback(Hello)
|
|
89
|
+
async def chose_language(ctx: Context, payload: Hello):
|
|
90
|
+
ctx.state.set(ASK_NAME)
|
|
91
|
+
ctx.state.update(lang=payload.lang)
|
|
92
|
+
await ctx.edit("Как тебя зовут?" if payload.lang == "ru" else "What's your name?")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dp.message(state=ASK_NAME)
|
|
96
|
+
async def got_name(ctx: Context):
|
|
97
|
+
lang = ctx.state.get("lang")
|
|
98
|
+
ctx.state.clear()
|
|
99
|
+
await ctx.reply(f"Приятно познакомиться, {escape(ctx.text)}!" if lang == "ru" else f"Nice to meet you, {escape(ctx.text)}!")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
dp.run_polling(MaxBot(MAX_TOKEN), TelegramBot(TG_TOKEN))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Можно запустить только один из ботов, если второй мессенджер не нужен. Пример побольше лежит в [examples/two_messengers.py](https://github.com/cvsqwe/matele/blob/main/examples/two_messengers.py).
|
|
106
|
+
|
|
107
|
+
## Кнопки
|
|
108
|
+
|
|
109
|
+
Данные кнопки описываются классом, а не строкой, которую потом приходится резать вручную. В хендлер приходит уже готовый объект с нужными типами:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
@callback("order:set")
|
|
113
|
+
class SetStatus(CallbackData):
|
|
114
|
+
order_id: int
|
|
115
|
+
status: Literal["paid", "shipped"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dp.callback(SetStatus)
|
|
119
|
+
async def set_status(ctx: Context, payload: SetStatus):
|
|
120
|
+
await ctx.edit(f"Заказ {payload.order_id}: {payload.status}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Telegram ограничивает данные кнопки 64 байтами. Если payload длиннее, matele подменяет его коротким ключом, а при нажатии возвращает исходное значение. Нажатия на кнопки из старых версий бота, которые уже не разбираются, приходят в `@dp.raw_callback()`, и бот не падает.
|
|
124
|
+
|
|
125
|
+
## Меню в одном сообщении
|
|
126
|
+
|
|
127
|
+
`ctx.show()` рисует экран в одном и том же сообщении: при нажатии кнопки оно редактируется, а не появляется новое. Если человек что-то напечатал, его сообщение удаляется, а меню обновляется прямо под ним, так что чат не засоряется.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
@dp.command("menu")
|
|
131
|
+
async def menu(ctx: Context):
|
|
132
|
+
await ctx.show("Главное меню", main_keyboard)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dp.callback(OpenOrders)
|
|
136
|
+
async def orders(ctx: Context, payload: OpenOrders):
|
|
137
|
+
await ctx.show("Ваши заказы", orders_keyboard, toast="Загружено")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Уведомления, которые не являются экранами меню, лучше отправлять обычным `ctx.reply()`.
|
|
141
|
+
|
|
142
|
+
## Фильтры и порядок хендлеров
|
|
143
|
+
|
|
144
|
+
Фильтр это любая функция от контекста, обычная или async. Фильтры комбинируются через `&`, `|` и `~`:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
is_admin = as_filter(lambda ctx: ctx.user.id in ADMINS)
|
|
148
|
+
|
|
149
|
+
@dp.command("ban", is_admin)
|
|
150
|
+
async def ban(ctx: Context): ...
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Встроенные фильтры: `Text`, `Regex`, `HasAttachment`, `OnPlatform`, `ChatType`.
|
|
154
|
+
|
|
155
|
+
Если у команды есть описание, при запуске она попадёт в меню команд в обоих мессенджерах. Команды без описания работают, но в меню не видны:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
@dp.command("start", description="Начать заново")
|
|
159
|
+
async def start(ctx: Context): ...
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Хендлер выбирается в предсказуемом порядке: сначала команды, потом хендлеры текущего состояния, потом все остальные. Поэтому `/cancel` срабатывает посреди любого сценария, и не нужно следить, в каком порядке что зарегистрировано.
|
|
163
|
+
|
|
164
|
+
## Состояния
|
|
165
|
+
|
|
166
|
+
`ctx.state` хранит, на каком шаге сценария находится человек, и данные, которые он успел ввести. По умолчанию всё лежит в памяти и теряется при перезапуске. Чтобы переживало перезапуск, подключите хранилище:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from matele.fsm import SQLiteStorage, RedisStorage
|
|
170
|
+
|
|
171
|
+
dp = Dispatcher(storage=SQLiteStorage("bot.db"))
|
|
172
|
+
# или, если процессов несколько:
|
|
173
|
+
dp = Dispatcher(storage=RedisStorage.from_url("redis://localhost:6379/0"))
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
SQLite работает на стандартной библиотеке, ничего ставить не нужно.
|
|
177
|
+
|
|
178
|
+
## Группы
|
|
179
|
+
|
|
180
|
+
Бот работает и в личке, и в групповых чатах. `ctx.reply()` отвечает туда, откуда пришло сообщение, а `ctx.send_private()` пишет человеку в личку. Состояние хранится отдельно для каждого человека в каждом чате, так что форма, начатая в одной группе, не мешает в другой.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
@dp.member_update()
|
|
184
|
+
async def welcome(ctx: Context):
|
|
185
|
+
update = ctx.member_update
|
|
186
|
+
if update.joined and not update.is_me:
|
|
187
|
+
await ctx.reply(f"Добро пожаловать, {escape(update.member.name)}!")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Команды вроде `/help@other_bot` бот пропускает: они адресованы не ему.
|
|
191
|
+
|
|
192
|
+
Чтобы бот видел в группе все сообщения, а не только команды: в Telegram выключите privacy mode у бота в BotFather, в MAX сделайте бота администратором с правом читать все сообщения.
|
|
193
|
+
|
|
194
|
+
## Файлы и ответы
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
await ctx.reply_file(Path("cat.jpg"), kind="image", caption="Кот")
|
|
198
|
+
await ctx.reply("Принято", quote=True) # ответом на сообщение человека
|
|
199
|
+
|
|
200
|
+
async with ctx.typing(): # «печатает…», пока идёт долгая работа
|
|
201
|
+
report = await build_report()
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Файл можно передать байтами, путём или открытым файлом. Большие файлы уходят потоком, целиком в память не читаются.
|
|
205
|
+
|
|
206
|
+
## Мини-приложения
|
|
207
|
+
|
|
208
|
+
Кнопка с `app=` открывает мини-приложение бота. Значение кнопки приложение получает как параметр запуска, по нему удобно сразу открыть нужную страницу:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
keyboard = Keyboard([[Button("Открыть магазин", app=""), Button("Сразу к кофе", app="coffee")]])
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
В MAX адрес мини-приложения задаётся в настройках бота на business.max.ru. В Telegram его передают в `TelegramBot(token, app_url="https://...")`, и там такие кнопки работают только в личных чатах.
|
|
215
|
+
|
|
216
|
+
Мини-приложение присылает на ваш сервер `initData`, подписанные мессенджером данные о человеке. Перед тем как чему-то верить, подпись нужно проверить:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
data = bot.verify_init_data(request.headers["X-Init-Data"]) # иначе InitDataError
|
|
220
|
+
await bot.send_message("Заказ принят", user_id=data.user.id)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Проверка одна на оба мессенджера: подпись и срок (по умолчанию час, как советует MAX). Готовый пример со страницей и сервером на FastAPI лежит в [examples/mini_app](https://github.com/cvsqwe/matele/tree/main/examples/mini_app).
|
|
224
|
+
|
|
225
|
+
## Рассылки
|
|
226
|
+
|
|
227
|
+
Если человек есть в обоих мессенджерах, `Hub` напишет ему в каждый. Отказ одного мессенджера не мешает второму:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from matele.hub import Hub
|
|
231
|
+
|
|
232
|
+
hub = Hub(max_bot, telegram_bot)
|
|
233
|
+
await hub.send({"max": 123, "telegram": 456}, "Заказ готов")
|
|
234
|
+
|
|
235
|
+
report = await hub.broadcast(subscribers(), "Мы открылись!")
|
|
236
|
+
print(report.reached, report.unreachable)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Скорость отправки ограничена под лимиты мессенджеров (по умолчанию 25 сообщений в секунду на каждый). Если мессенджер ненадолго пропал, отправка подождёт и повторит, а при долгом сбое сразу вернёт ошибку и не застопорит рассылку. Тех, кто заблокировал бота, видно в `report.unreachable`, а `on_unreachable` позволяет сразу отметить их у себя. Получателей можно отдавать генератором, например постранично из базы.
|
|
240
|
+
|
|
241
|
+
## Вебхуки
|
|
242
|
+
|
|
243
|
+
Вместо постоянного опроса мессенджеры могут сами присылать события на ваш сервер:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from matele.webhook import Webhook
|
|
247
|
+
|
|
248
|
+
webhook = Webhook(dp, base_url="https://bot.example.com")
|
|
249
|
+
webhook.add(MaxBot(MAX_TOKEN), "/max")
|
|
250
|
+
webhook.add(TelegramBot(TG_TOKEN), "/telegram")
|
|
251
|
+
webhook.run(port=8080)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`Webhook` это обычное ASGI-приложение, его можно запустить и через `uvicorn app:webhook`. При старте он сам регистрирует адреса в мессенджерах со случайным секретом и проверяет этот секрет в каждом запросе. Нужен публичный HTTPS-адрес: MAX принимает только порт 443, Telegram ещё 80, 88 и 8443.
|
|
255
|
+
|
|
256
|
+
## Весь MAX Bot API
|
|
257
|
+
|
|
258
|
+
Помимо общего слоя есть типизированный клиент ко всем 33 методам MAX Bot API: чаты, участники, закрепы, загрузка файлов, вебхуки, комментарии.
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from matele.max import MaxApi
|
|
262
|
+
|
|
263
|
+
async with MaxApi(token) as api:
|
|
264
|
+
photo = await api.upload(Path("cat.jpg"), type="image")
|
|
265
|
+
sent = await api.send_message(chat_id=chat_id, text="<b>Кот</b>", format="html", attachments=[photo])
|
|
266
|
+
await api.pin_message(chat_id, sent.body.mid)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Модели генерируются из [официальной схемы](https://github.com/max-messenger/api-schema), поэтому совпадают с документацией MAX. Если MAX добавит новое поле или новый тип события, бот не упадёт: неизвестное просто приходит как базовый объект с исходными данными в `.raw`. Сертификат Минцифры, которым подписан MAX API, уже встроен и используется только для запросов к MAX.
|
|
270
|
+
|
|
271
|
+
Из хендлера клиент доступен как `ctx.bot.api`. Для Telegram есть `TelegramApi.call()`, через него можно вызвать любой метод Bot API.
|
|
272
|
+
|
|
273
|
+
## Тесты
|
|
274
|
+
|
|
275
|
+
Ботов можно тестировать без сети и токенов:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
from matele.testing import FakeBot
|
|
279
|
+
|
|
280
|
+
bot = FakeBot()
|
|
281
|
+
await dp.feed(bot.message("/start"))
|
|
282
|
+
assert bot.sent[-1].text == "Привет! На каком языке говорим?"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Что дальше
|
|
286
|
+
|
|
287
|
+
Ближайшее: проверка на живых ботах в обоих мессенджерах. Подробнее в [ROADMAP.md](https://github.com/cvsqwe/matele/blob/main/ROADMAP.md).
|
|
288
|
+
|
|
289
|
+
## Разработка
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
python -m venv .venv
|
|
293
|
+
.venv/bin/python -m pip install -e ".[dev]"
|
|
294
|
+
.venv/bin/python -m pytest
|
|
295
|
+
.venv/bin/ruff check src tests tools examples
|
|
296
|
+
.venv/bin/basedpyright
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Документация лежит в `docs/` и собирается MkDocs:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
.venv/bin/python -m pip install -e ".[docs]"
|
|
303
|
+
.venv/bin/mkdocs serve
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
В коде отступы табами, настройки в `.editorconfig`.
|
|
307
|
+
|
|
308
|
+
## Лицензия
|
|
309
|
+
|
|
310
|
+
MIT
|