aiogram-webhook 2.0.1__tar.gz → 3.0.1__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.
- aiogram_webhook-3.0.1/PKG-INFO +94 -0
- aiogram_webhook-3.0.1/README.md +55 -0
- {aiogram_webhook-2.0.1 → aiogram_webhook-3.0.1}/pyproject.toml +9 -6
- aiogram_webhook-3.0.1/src/aiogram_webhook/__init__.py +16 -0
- {aiogram_webhook-2.0.1/src/aiogram_webhook/config → aiogram_webhook-3.0.1/src/aiogram_webhook/configs}/bot.py +1 -1
- {aiogram_webhook-2.0.1/src/aiogram_webhook/config → aiogram_webhook-3.0.1/src/aiogram_webhook/configs}/webhook.py +11 -9
- aiogram_webhook-3.0.1/src/aiogram_webhook/engines/base.py +153 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/engines/errors.py +77 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/engines/multi.py +74 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/engines/single.py +71 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/engines/target.py +7 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/engines/token.py +131 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/errors.py +16 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/logs.py +20 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/route/__init__.py +5 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/route/config.py +29 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/route/errors.py +213 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/route/params.py +43 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/route/path.py +87 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/route/query.py +148 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/route/route.py +98 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/route/url.py +18 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/security/checks/check.py +20 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/security/checks/ip.py +83 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/security/errors.py +42 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/security/secret_token.py +64 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/security/security.py +38 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/tasks.py +58 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/utils/_payload.py +36 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/utils/config.py +52 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/web/__init__.py +3 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/web/_starlette.py +55 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/web/aiohttp.py +89 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/web/base.py +79 -0
- aiogram_webhook-3.0.1/src/aiogram_webhook/web/fastapi.py +83 -0
- aiogram_webhook-2.0.1/PKG-INFO +0 -396
- aiogram_webhook-2.0.1/README.md +0 -361
- aiogram_webhook-2.0.1/src/aiogram_webhook/__init__.py +0 -21
- aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/aiohttp/adapter.py +0 -56
- aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/aiohttp/mapping.py +0 -15
- aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/base_adapter.py +0 -86
- aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/base_mapping.py +0 -38
- aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/fastapi/adapter.py +0 -49
- aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/fastapi/mapping.py +0 -15
- aiogram_webhook-2.0.1/src/aiogram_webhook/engines/__init__.py +0 -5
- aiogram_webhook-2.0.1/src/aiogram_webhook/engines/base.py +0 -158
- aiogram_webhook-2.0.1/src/aiogram_webhook/engines/simple.py +0 -101
- aiogram_webhook-2.0.1/src/aiogram_webhook/engines/token.py +0 -126
- aiogram_webhook-2.0.1/src/aiogram_webhook/routing/__init__.py +0 -6
- aiogram_webhook-2.0.1/src/aiogram_webhook/routing/base.py +0 -38
- aiogram_webhook-2.0.1/src/aiogram_webhook/routing/path.py +0 -28
- aiogram_webhook-2.0.1/src/aiogram_webhook/routing/query.py +0 -18
- aiogram_webhook-2.0.1/src/aiogram_webhook/routing/static.py +0 -12
- aiogram_webhook-2.0.1/src/aiogram_webhook/security/checks/check.py +0 -17
- aiogram_webhook-2.0.1/src/aiogram_webhook/security/checks/ip.py +0 -83
- aiogram_webhook-2.0.1/src/aiogram_webhook/security/secret_token.py +0 -54
- aiogram_webhook-2.0.1/src/aiogram_webhook/security/security.py +0 -44
- {aiogram_webhook-2.0.1/src/aiogram_webhook/adapters → aiogram_webhook-3.0.1/src/aiogram_webhook/configs}/__init__.py +0 -0
- {aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/aiohttp → aiogram_webhook-3.0.1/src/aiogram_webhook/engines}/__init__.py +0 -0
- /aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/fastapi/__init__.py → /aiogram_webhook-3.0.1/src/aiogram_webhook/py.typed +0 -0
- {aiogram_webhook-2.0.1 → aiogram_webhook-3.0.1}/src/aiogram_webhook/security/__init__.py +0 -0
- {aiogram_webhook-2.0.1/src/aiogram_webhook/config → aiogram_webhook-3.0.1/src/aiogram_webhook/security/checks}/__init__.py +0 -0
- {aiogram_webhook-2.0.1/src/aiogram_webhook/security/checks → aiogram_webhook-3.0.1/src/aiogram_webhook/utils}/__init__.py +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: aiogram-webhook
|
|
3
|
+
Version: 3.0.1
|
|
4
|
+
Summary: A python library for integrating webhook support with multiple web frameworks in aiogram. Organizes bot operation via webhooks for both single and multi-bot setups.
|
|
5
|
+
Keywords: aiogram,multibot,telegram,webhook,fastapi,aiohttp
|
|
6
|
+
Author: m-xim
|
|
7
|
+
Author-email: m-xim <i@m-xim.ru>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: aiogram>=3.14.0
|
|
19
|
+
Requires-Dist: yarl>=1.17.0
|
|
20
|
+
Requires-Dist: multidict>=6.7.1
|
|
21
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
22
|
+
Requires-Dist: aiohttp>=3.9.0 ; extra == 'aiohttp'
|
|
23
|
+
Requires-Dist: ruff ; extra == 'dev'
|
|
24
|
+
Requires-Dist: ty ; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest ; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio ; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov ; extra == 'dev'
|
|
28
|
+
Requires-Dist: httpx2 ; extra == 'dev'
|
|
29
|
+
Requires-Dist: fastapi>=0.128.0 ; extra == 'fastapi'
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Project-URL: Homepage, https://github.com/m-xim/aiogram-webhook
|
|
32
|
+
Project-URL: Repository, https://github.com/m-xim/aiogram-webhook
|
|
33
|
+
Project-URL: Issues, https://github.com/m-xim/aiogram-webhook/issues
|
|
34
|
+
Project-URL: Documentation, https://aiogram-webhook.m-xim.ru
|
|
35
|
+
Provides-Extra: aiohttp
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Provides-Extra: fastapi
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
# aiogram-webhook
|
|
43
|
+
[](https://pypi.org/project/aiogram-webhook)
|
|
44
|
+
[](https://codecov.io/github/m-xim/aiogram-webhook)
|
|
45
|
+
[](https://github.com/m-xim/aiogram-webhook/actions)
|
|
46
|
+
[](/LICENSE)
|
|
47
|
+
[](https://deepwiki.com/m-xim/aiogram-webhook)
|
|
48
|
+
[](https://github.com/astral-sh/ruff)
|
|
49
|
+
[](https://github.com/astral-sh/ty)
|
|
50
|
+
|
|
51
|
+
`aiogram-webhook` is a modular Python library for webhook integration in aiogram.
|
|
52
|
+
It supports single-bot and token-based multi-bot setups, with route building, optional request checks, and adapters for FastAPI and aiohttp.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install aiogram-webhook
|
|
58
|
+
pip install "aiogram-webhook[fastapi]"
|
|
59
|
+
pip install "aiogram-webhook[aiohttp]"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from aiogram import Bot, Dispatcher
|
|
66
|
+
from fastapi import FastAPI
|
|
67
|
+
|
|
68
|
+
from aiogram_webhook import FastAPIAdapter, SingleBotEngine
|
|
69
|
+
from aiogram_webhook.route import Route
|
|
70
|
+
|
|
71
|
+
dispatcher = Dispatcher()
|
|
72
|
+
bot = Bot("BOT_TOKEN")
|
|
73
|
+
|
|
74
|
+
engine = SingleBotEngine(
|
|
75
|
+
dispatcher,
|
|
76
|
+
bot,
|
|
77
|
+
web=FastAPIAdapter(),
|
|
78
|
+
route=Route(base_url="https://example.com", path="/webhook"),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
app = FastAPI()
|
|
82
|
+
engine.register(app)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Call `await engine.set_webhook()` during your application startup to register the public webhook URL in Telegram.
|
|
86
|
+
For production, pass `security=Security(...)` to verify Telegram requests.
|
|
87
|
+
|
|
88
|
+
## Documentation
|
|
89
|
+
|
|
90
|
+
The full documentation is in [`docs`](https://aiogram-webhook.m-xim.ru). It covers installation, FastAPI and aiohttp setup, routing, security, lifecycle behavior, and the public API.
|
|
91
|
+
|
|
92
|
+
## Contributing
|
|
93
|
+
|
|
94
|
+
See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for development setup, branch naming, commit conventions, and PR guidelines.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# aiogram-webhook
|
|
4
|
+
[](https://pypi.org/project/aiogram-webhook)
|
|
5
|
+
[](https://codecov.io/github/m-xim/aiogram-webhook)
|
|
6
|
+
[](https://github.com/m-xim/aiogram-webhook/actions)
|
|
7
|
+
[](/LICENSE)
|
|
8
|
+
[](https://deepwiki.com/m-xim/aiogram-webhook)
|
|
9
|
+
[](https://github.com/astral-sh/ruff)
|
|
10
|
+
[](https://github.com/astral-sh/ty)
|
|
11
|
+
|
|
12
|
+
`aiogram-webhook` is a modular Python library for webhook integration in aiogram.
|
|
13
|
+
It supports single-bot and token-based multi-bot setups, with route building, optional request checks, and adapters for FastAPI and aiohttp.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install aiogram-webhook
|
|
19
|
+
pip install "aiogram-webhook[fastapi]"
|
|
20
|
+
pip install "aiogram-webhook[aiohttp]"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from aiogram import Bot, Dispatcher
|
|
27
|
+
from fastapi import FastAPI
|
|
28
|
+
|
|
29
|
+
from aiogram_webhook import FastAPIAdapter, SingleBotEngine
|
|
30
|
+
from aiogram_webhook.route import Route
|
|
31
|
+
|
|
32
|
+
dispatcher = Dispatcher()
|
|
33
|
+
bot = Bot("BOT_TOKEN")
|
|
34
|
+
|
|
35
|
+
engine = SingleBotEngine(
|
|
36
|
+
dispatcher,
|
|
37
|
+
bot,
|
|
38
|
+
web=FastAPIAdapter(),
|
|
39
|
+
route=Route(base_url="https://example.com", path="/webhook"),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
app = FastAPI()
|
|
43
|
+
engine.register(app)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Call `await engine.set_webhook()` during your application startup to register the public webhook URL in Telegram.
|
|
47
|
+
For production, pass `security=Security(...)` to verify Telegram requests.
|
|
48
|
+
|
|
49
|
+
## Documentation
|
|
50
|
+
|
|
51
|
+
The full documentation is in [`docs`](https://aiogram-webhook.m-xim.ru). It covers installation, FastAPI and aiohttp setup, routing, security, lifecycle behavior, and the public API.
|
|
52
|
+
|
|
53
|
+
## Contributing
|
|
54
|
+
|
|
55
|
+
See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for development setup, branch naming, commit conventions, and PR guidelines.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aiogram-webhook"
|
|
3
|
-
version = "
|
|
3
|
+
version = "3.0.1"
|
|
4
4
|
description = "A python library for integrating webhook support with multiple web frameworks in aiogram. Organizes bot operation via webhooks for both single and multi-bot setups."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -9,15 +9,17 @@ requires-python = ">=3.10"
|
|
|
9
9
|
dependencies = [
|
|
10
10
|
"aiogram>=3.14.0",
|
|
11
11
|
"yarl>=1.17.0",
|
|
12
|
+
"multidict>=6.7.1",
|
|
13
|
+
"aiohttp>=3.9.0",
|
|
12
14
|
]
|
|
13
15
|
|
|
14
16
|
keywords = [
|
|
15
17
|
"aiogram",
|
|
18
|
+
"multibot",
|
|
16
19
|
"telegram",
|
|
17
20
|
"webhook",
|
|
18
21
|
"fastapi",
|
|
19
22
|
"aiohttp",
|
|
20
|
-
"multibot",
|
|
21
23
|
]
|
|
22
24
|
classifiers = [
|
|
23
25
|
"Programming Language :: Python :: 3",
|
|
@@ -28,13 +30,14 @@ classifiers = [
|
|
|
28
30
|
"Programming Language :: Python :: 3.14",
|
|
29
31
|
"Topic :: Software Development :: Libraries",
|
|
30
32
|
"Operating System :: OS Independent",
|
|
33
|
+
"Typing :: Typed",
|
|
31
34
|
]
|
|
32
35
|
|
|
33
36
|
[project.urls]
|
|
34
37
|
Homepage = "https://github.com/m-xim/aiogram-webhook"
|
|
35
38
|
Repository = "https://github.com/m-xim/aiogram-webhook"
|
|
36
39
|
Issues = "https://github.com/m-xim/aiogram-webhook/issues"
|
|
37
|
-
Documentation = "https://
|
|
40
|
+
Documentation = "https://aiogram-webhook.m-xim.ru"
|
|
38
41
|
|
|
39
42
|
[project.optional-dependencies]
|
|
40
43
|
fastapi = [
|
|
@@ -48,17 +51,17 @@ dev = [
|
|
|
48
51
|
"ty",
|
|
49
52
|
"pytest",
|
|
50
53
|
"pytest-asyncio",
|
|
51
|
-
"
|
|
54
|
+
"pytest-cov",
|
|
55
|
+
"httpx2",
|
|
52
56
|
]
|
|
53
57
|
|
|
54
58
|
[build-system]
|
|
55
|
-
requires = ["uv_build"]
|
|
59
|
+
requires = ["uv_build<0.12"]
|
|
56
60
|
build-backend = "uv_build"
|
|
57
61
|
|
|
58
62
|
[tool.uv]
|
|
59
63
|
package = true
|
|
60
64
|
|
|
61
|
-
|
|
62
65
|
[tool.semantic_release]
|
|
63
66
|
version_source = "pyproject"
|
|
64
67
|
commit_message = "chore(release): {version}"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from aiogram_webhook.configs.bot import BotConfig
|
|
2
|
+
from aiogram_webhook.configs.webhook import WebhookConfig
|
|
3
|
+
from aiogram_webhook.engines.single import SingleBotEngine
|
|
4
|
+
from aiogram_webhook.engines.token import TokenEngine
|
|
5
|
+
from aiogram_webhook.web.aiohttp import AiohttpAdapter
|
|
6
|
+
|
|
7
|
+
__all__ = ["AiohttpAdapter", "BotConfig", "SingleBotEngine", "TokenEngine", "WebhookConfig"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from aiogram_webhook.web.fastapi import FastAPIAdapter # noqa: F401
|
|
12
|
+
|
|
13
|
+
__all__.insert(2, "FastAPIAdapter")
|
|
14
|
+
except ModuleNotFoundError as exc:
|
|
15
|
+
if exc.name != "fastapi":
|
|
16
|
+
raise
|
|
@@ -4,7 +4,7 @@ from aiogram.client.default import DefaultBotProperties
|
|
|
4
4
|
from aiogram.client.session.base import BaseSession
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
@dataclass
|
|
7
|
+
@dataclass(slots=True)
|
|
8
8
|
class BotConfig:
|
|
9
9
|
session: BaseSession | None = None
|
|
10
10
|
"""HTTP Client session (For example AiohttpSession). If not specified it will be automatically created."""
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
1
3
|
from aiogram.types import InputFile
|
|
2
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
3
4
|
|
|
5
|
+
from aiogram_webhook.utils.config import OMITTED, Omittable
|
|
4
6
|
|
|
5
|
-
class WebhookConfig(BaseModel):
|
|
6
|
-
"""Webhook configuration for setWebhook API parameters."""
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class WebhookConfig:
|
|
10
|
+
"""Webhook configuration for setWebhook API parameters."""
|
|
9
11
|
|
|
10
|
-
certificate: InputFile | None =
|
|
12
|
+
certificate: Omittable[InputFile | None] = OMITTED
|
|
11
13
|
"""Upload your public key certificate so that the root certificate in use can be checked. See our `self-signed guide <https://core.telegram.org/bots/self-signed>`_ for details."""
|
|
12
|
-
ip_address: str | None =
|
|
14
|
+
ip_address: Omittable[str | None] = OMITTED
|
|
13
15
|
"""The fixed IP address which will be used to send webhook requests instead of the IP address resolved through DNS"""
|
|
14
|
-
max_connections: int | None =
|
|
16
|
+
max_connections: Omittable[int | None] = OMITTED
|
|
15
17
|
"""The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to *40*. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput."""
|
|
16
|
-
allowed_updates: list[str] | None =
|
|
18
|
+
allowed_updates: Omittable[list[str] | None] = OMITTED
|
|
17
19
|
"""A JSON-serialized list of the update types you want your bot to receive. For example, specify :code:`["message", "edited_channel_post", "callback_query"]` to only receive updates of these types. See :class:`aiogram.types.update.Update` for a complete list of available update types. Specify an empty list to receive all update types except *chat_member*, *message_reaction*, and *message_reaction_count* (default). If not specified, the previous setting will be used."""
|
|
18
|
-
drop_pending_updates: bool | None =
|
|
20
|
+
drop_pending_updates: Omittable[bool | None] = OMITTED
|
|
19
21
|
"""Pass :code:`True` to drop all pending updates"""
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from aiogram import Bot, Dispatcher
|
|
8
|
+
from aiogram.methods import TelegramMethod
|
|
9
|
+
|
|
10
|
+
from aiogram_webhook.engines.errors import (
|
|
11
|
+
BotNotFoundError,
|
|
12
|
+
InvalidJsonError,
|
|
13
|
+
RequestHandlingStoppedError,
|
|
14
|
+
TargetNotFoundError,
|
|
15
|
+
)
|
|
16
|
+
from aiogram_webhook.engines.target import Target
|
|
17
|
+
from aiogram_webhook.errors import AiogramWebhookError
|
|
18
|
+
from aiogram_webhook.logs import get_logger, log_webhook_error
|
|
19
|
+
from aiogram_webhook.route import Route
|
|
20
|
+
from aiogram_webhook.route.params import RouteParams
|
|
21
|
+
from aiogram_webhook.security import Security
|
|
22
|
+
from aiogram_webhook.tasks import TaskTracker
|
|
23
|
+
from aiogram_webhook.utils._payload import build_webhook_payload
|
|
24
|
+
from aiogram_webhook.web.base import WebAdapter, WebRequest
|
|
25
|
+
|
|
26
|
+
logger = get_logger("engines")
|
|
27
|
+
|
|
28
|
+
AppT = TypeVar("AppT")
|
|
29
|
+
RawRequestT = TypeVar("RawRequestT")
|
|
30
|
+
FrameworkResponseT = TypeVar("FrameworkResponseT")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BaseWebhookEngine(ABC, Generic[AppT, RawRequestT, FrameworkResponseT]):
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
dispatcher: Dispatcher,
|
|
37
|
+
web: WebAdapter[AppT, RawRequestT, FrameworkResponseT],
|
|
38
|
+
route: Route,
|
|
39
|
+
security: Security | None = None,
|
|
40
|
+
handle_in_background: bool = True,
|
|
41
|
+
shutdown_timeout: float = 10.0,
|
|
42
|
+
) -> None:
|
|
43
|
+
self.dispatcher = dispatcher
|
|
44
|
+
self.web = web
|
|
45
|
+
self.route = route
|
|
46
|
+
self.security = security
|
|
47
|
+
self.handle_in_background = handle_in_background
|
|
48
|
+
|
|
49
|
+
self.shutdown_timeout = shutdown_timeout
|
|
50
|
+
self._is_shutting_down = False
|
|
51
|
+
|
|
52
|
+
if self.security is None:
|
|
53
|
+
warnings.warn(
|
|
54
|
+
f"Security is not configured for {type(self).__name__}. Pass security=... to verify webhook requests.",
|
|
55
|
+
UserWarning,
|
|
56
|
+
stacklevel=2,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def register(self, app: AppT) -> None:
|
|
60
|
+
logger.info("Registering webhook path %s via %s", self.route.path, self.web.__class__.__name__)
|
|
61
|
+
self.web.register(
|
|
62
|
+
app=app,
|
|
63
|
+
path=self.route.path,
|
|
64
|
+
handler=self.handle_request,
|
|
65
|
+
on_startup=self.on_startup,
|
|
66
|
+
on_shutdown=self.on_shutdown,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
async def handle_request(self, request: WebRequest[RawRequestT]) -> FrameworkResponseT:
|
|
70
|
+
try:
|
|
71
|
+
if self._is_shutting_down:
|
|
72
|
+
raise RequestHandlingStoppedError
|
|
73
|
+
|
|
74
|
+
route_params = await self.route.match(request)
|
|
75
|
+
|
|
76
|
+
target = await self._resolve_target(request=request, route_params=route_params)
|
|
77
|
+
if target is None:
|
|
78
|
+
raise TargetNotFoundError(route_param_names=route_params.keys())
|
|
79
|
+
|
|
80
|
+
if self.security is not None:
|
|
81
|
+
await self.security.verify(target=target, request=request, route_params=route_params)
|
|
82
|
+
|
|
83
|
+
bot = await self._resolve_bot(target=target)
|
|
84
|
+
if bot is None:
|
|
85
|
+
raise BotNotFoundError(target_bot_id=target.bot_id, target_type=target.__class__.__name__)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
update = await request.json()
|
|
89
|
+
except ValueError as exc:
|
|
90
|
+
raise InvalidJsonError(original_error=exc) from exc
|
|
91
|
+
|
|
92
|
+
if self.handle_in_background:
|
|
93
|
+
self._get_task_tracker(bot).spawn(self._background_feed(bot, update))
|
|
94
|
+
else:
|
|
95
|
+
result = await self.dispatcher.feed_webhook_update(bot=bot, update=update)
|
|
96
|
+
if isinstance(result, TelegramMethod):
|
|
97
|
+
return self.web.payload_response(status_code=200, payload=build_webhook_payload(bot, result))
|
|
98
|
+
|
|
99
|
+
return self.web.json_response(status_code=200, data={})
|
|
100
|
+
|
|
101
|
+
except AiogramWebhookError as exc:
|
|
102
|
+
log_webhook_error(logger, exc)
|
|
103
|
+
|
|
104
|
+
return self.web.json_response(status_code=exc.status_code, data=exc.response_payload())
|
|
105
|
+
|
|
106
|
+
async def on_startup(self, app: AppT, *args: Any, **kwargs: Any) -> None:
|
|
107
|
+
await self._on_startup(app, *args, **kwargs)
|
|
108
|
+
self._is_shutting_down = False
|
|
109
|
+
|
|
110
|
+
async def on_shutdown(self, app: AppT, *args: Any, **kwargs: Any) -> None:
|
|
111
|
+
self._is_shutting_down = True
|
|
112
|
+
await self._on_shutdown(app, *args, **kwargs)
|
|
113
|
+
|
|
114
|
+
@asynccontextmanager
|
|
115
|
+
async def lifespan(self, app: AppT) -> AsyncGenerator[None, Any]:
|
|
116
|
+
try:
|
|
117
|
+
await self.on_startup(app=app)
|
|
118
|
+
yield
|
|
119
|
+
finally:
|
|
120
|
+
await self.on_shutdown(app=app)
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
async def _on_startup(self, app: AppT, *args: Any, **kwargs: Any) -> None:
|
|
124
|
+
raise NotImplementedError
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
async def _on_shutdown(self, app: AppT, *args: Any, **kwargs: Any) -> None:
|
|
128
|
+
raise NotImplementedError
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
async def _resolve_target(self, request: WebRequest[RawRequestT], route_params: RouteParams) -> Target | None: ...
|
|
132
|
+
|
|
133
|
+
@abstractmethod
|
|
134
|
+
async def _resolve_bot(self, target: Target) -> Bot | None: ...
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def _get_task_tracker(self, bot: Bot) -> TaskTracker:
|
|
138
|
+
raise NotImplementedError
|
|
139
|
+
|
|
140
|
+
async def _background_feed(self, bot: Bot, update: dict[str, Any]) -> None:
|
|
141
|
+
result = await self.dispatcher.feed_raw_update(bot=bot, update=update)
|
|
142
|
+
|
|
143
|
+
if isinstance(result, TelegramMethod):
|
|
144
|
+
await self.dispatcher.silent_call_request(bot=bot, result=result)
|
|
145
|
+
|
|
146
|
+
def _build_lifecycle_data(self, *, app: AppT, **kwargs) -> dict[str, Any]:
|
|
147
|
+
return {
|
|
148
|
+
"dispatcher": self.dispatcher,
|
|
149
|
+
**self.dispatcher.workflow_data,
|
|
150
|
+
"app": app,
|
|
151
|
+
"webhook_engine": self,
|
|
152
|
+
**kwargs,
|
|
153
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
|
|
4
|
+
from aiogram_webhook.errors import AiogramWebhookError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def format_names(names: Iterable[str]) -> str:
|
|
8
|
+
return ", ".join(repr(name) for name in sorted(names))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EngineError(AiogramWebhookError):
|
|
12
|
+
code = "engine_error"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TargetNotFoundError(EngineError):
|
|
16
|
+
code = "engine_target_not_found"
|
|
17
|
+
status_code = 404
|
|
18
|
+
public_detail = "Not found"
|
|
19
|
+
log_level = logging.INFO
|
|
20
|
+
|
|
21
|
+
def __init__(self, *, route_param_names: Iterable[str]) -> None:
|
|
22
|
+
self.route_param_names = tuple(sorted(route_param_names))
|
|
23
|
+
|
|
24
|
+
super().__init__(f"Webhook target was not found. Route param names: {format_names(self.route_param_names)}.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BotNotFoundError(EngineError):
|
|
28
|
+
code = "engine_bot_not_found"
|
|
29
|
+
status_code = 404
|
|
30
|
+
public_detail = "Not found"
|
|
31
|
+
log_level = logging.ERROR
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
target_bot_id: int | None = None,
|
|
37
|
+
target_type: str | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.target_bot_id = target_bot_id
|
|
40
|
+
self.target_type = target_type
|
|
41
|
+
|
|
42
|
+
message = "Webhook bot was not found."
|
|
43
|
+
|
|
44
|
+
if target_bot_id is not None:
|
|
45
|
+
message += f" Target bot id: {target_bot_id}."
|
|
46
|
+
|
|
47
|
+
if target_type is not None:
|
|
48
|
+
message += f" Target type: {target_type!r}."
|
|
49
|
+
|
|
50
|
+
super().__init__(message)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class InvalidJsonError(EngineError):
|
|
54
|
+
code = "engine_invalid_json"
|
|
55
|
+
status_code = 400
|
|
56
|
+
public_detail = "Bad request"
|
|
57
|
+
log_level = logging.ERROR
|
|
58
|
+
|
|
59
|
+
def __init__(self, *, original_error: BaseException | None = None) -> None:
|
|
60
|
+
self.original_error_type = type(original_error).__name__ if original_error is not None else None
|
|
61
|
+
|
|
62
|
+
message = "Invalid webhook JSON payload."
|
|
63
|
+
|
|
64
|
+
if self.original_error_type is not None:
|
|
65
|
+
message += f" Original error type: {self.original_error_type}."
|
|
66
|
+
|
|
67
|
+
super().__init__(message)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RequestHandlingStoppedError(EngineError):
|
|
71
|
+
code = "engine_request_handling_stopped"
|
|
72
|
+
status_code = 503
|
|
73
|
+
public_detail = "Service unavailable"
|
|
74
|
+
log_level = logging.DEBUG
|
|
75
|
+
|
|
76
|
+
def __init__(self) -> None:
|
|
77
|
+
super().__init__("Webhook engine is shutting down and no longer accepts requests.")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from collections.abc import Iterable, Mapping
|
|
3
|
+
from types import MappingProxyType
|
|
4
|
+
from typing import Any, Generic
|
|
5
|
+
|
|
6
|
+
from aiogram import Bot
|
|
7
|
+
|
|
8
|
+
from aiogram_webhook import WebhookConfig
|
|
9
|
+
from aiogram_webhook.engines.base import AppT, BaseWebhookEngine, FrameworkResponseT, RawRequestT, logger
|
|
10
|
+
from aiogram_webhook.engines.target import Target
|
|
11
|
+
from aiogram_webhook.route import Route
|
|
12
|
+
from aiogram_webhook.security import Security
|
|
13
|
+
from aiogram_webhook.tasks import TaskTracker
|
|
14
|
+
from aiogram_webhook.utils.config import dataclass_config_to_kwargs
|
|
15
|
+
from aiogram_webhook.web.base import WebAdapter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseMultiBotEngine(
|
|
19
|
+
BaseWebhookEngine[AppT, RawRequestT, FrameworkResponseT], ABC, Generic[AppT, RawRequestT, FrameworkResponseT]
|
|
20
|
+
):
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
dispatcher,
|
|
24
|
+
web: WebAdapter[AppT, RawRequestT, FrameworkResponseT],
|
|
25
|
+
route: Route,
|
|
26
|
+
security: Security | None = None,
|
|
27
|
+
webhook_config: WebhookConfig | None = None,
|
|
28
|
+
handle_in_background: bool = True,
|
|
29
|
+
shutdown_timeout: float = 10.0,
|
|
30
|
+
) -> None:
|
|
31
|
+
self._task_trackers: dict[int, TaskTracker] = {}
|
|
32
|
+
self._bots: dict[int, Bot] = {}
|
|
33
|
+
self.webhook_config = webhook_config or WebhookConfig()
|
|
34
|
+
super().__init__(
|
|
35
|
+
dispatcher=dispatcher,
|
|
36
|
+
web=web,
|
|
37
|
+
route=route,
|
|
38
|
+
security=security,
|
|
39
|
+
handle_in_background=handle_in_background,
|
|
40
|
+
shutdown_timeout=shutdown_timeout,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async def _build_webhook_kwargs(
|
|
44
|
+
self, target: Target, webhook_config: WebhookConfig | None = None
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
kwargs = dataclass_config_to_kwargs(self.webhook_config, webhook_config)
|
|
47
|
+
if self.security is not None:
|
|
48
|
+
secret_token = await self.security.secret_token(target)
|
|
49
|
+
if secret_token is not None:
|
|
50
|
+
kwargs["secret_token"] = secret_token
|
|
51
|
+
return kwargs
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def bots(self) -> Mapping[int, Bot]:
|
|
55
|
+
return MappingProxyType(self._bots)
|
|
56
|
+
|
|
57
|
+
async def _on_startup(self, app: AppT, *args: Any, bots: Iterable[Bot] | None = None, **kwargs: Any) -> None: # noqa: ARG002
|
|
58
|
+
all_bots = set(self.bots.values())
|
|
59
|
+
|
|
60
|
+
if bots is not None:
|
|
61
|
+
all_bots |= set(bots)
|
|
62
|
+
|
|
63
|
+
logger.info("Starting multi-bot webhook engine with %s bot(s)", len(all_bots))
|
|
64
|
+
lifecycle_data = self._build_lifecycle_data(app=app, bots=all_bots, **kwargs)
|
|
65
|
+
await self.dispatcher.emit_startup(**lifecycle_data)
|
|
66
|
+
|
|
67
|
+
def _get_task_tracker(self, bot: Bot) -> TaskTracker:
|
|
68
|
+
tracker = self._task_trackers.get(bot.id)
|
|
69
|
+
|
|
70
|
+
if tracker is None:
|
|
71
|
+
tracker = TaskTracker()
|
|
72
|
+
self._task_trackers[bot.id] = tracker
|
|
73
|
+
|
|
74
|
+
return tracker
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Generic
|
|
2
|
+
|
|
3
|
+
from aiogram import Bot
|
|
4
|
+
|
|
5
|
+
from aiogram_webhook.configs.webhook import WebhookConfig
|
|
6
|
+
from aiogram_webhook.engines.base import AppT, BaseWebhookEngine, FrameworkResponseT, RawRequestT, logger
|
|
7
|
+
from aiogram_webhook.engines.target import Target
|
|
8
|
+
from aiogram_webhook.route import Route
|
|
9
|
+
from aiogram_webhook.route.params import RouteParams
|
|
10
|
+
from aiogram_webhook.tasks import TaskTracker
|
|
11
|
+
from aiogram_webhook.utils.config import dataclass_config_to_kwargs
|
|
12
|
+
from aiogram_webhook.web.base import WebAdapter, WebRequest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SingleBotEngine(
|
|
16
|
+
BaseWebhookEngine[AppT, RawRequestT, FrameworkResponseT], Generic[AppT, RawRequestT, FrameworkResponseT]
|
|
17
|
+
):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
dispatcher,
|
|
21
|
+
bot: Bot,
|
|
22
|
+
web: WebAdapter[AppT, RawRequestT, FrameworkResponseT],
|
|
23
|
+
route: Route,
|
|
24
|
+
security=None,
|
|
25
|
+
handle_in_background: bool = True,
|
|
26
|
+
shutdown_timeout: float = 10.0,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.bot = bot
|
|
29
|
+
self._task_tracker = TaskTracker()
|
|
30
|
+
|
|
31
|
+
super().__init__(
|
|
32
|
+
dispatcher=dispatcher,
|
|
33
|
+
web=web,
|
|
34
|
+
route=route,
|
|
35
|
+
security=security,
|
|
36
|
+
handle_in_background=handle_in_background,
|
|
37
|
+
shutdown_timeout=shutdown_timeout,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async def _resolve_target(self, request: WebRequest[RawRequestT], route_params: RouteParams) -> Target | None: # noqa: ARG002
|
|
41
|
+
return Target(bot_id=self.bot.id, bot_token=self.bot.token)
|
|
42
|
+
|
|
43
|
+
async def _resolve_bot(self, target: Target) -> Bot: # noqa: ARG002
|
|
44
|
+
return self.bot
|
|
45
|
+
|
|
46
|
+
def _get_task_tracker(self, bot: Bot) -> TaskTracker: # noqa: ARG002
|
|
47
|
+
return self._task_tracker
|
|
48
|
+
|
|
49
|
+
async def set_webhook(self, webhook_config: WebhookConfig | None = None) -> bool:
|
|
50
|
+
target = Target(bot_id=self.bot.id, bot_token=self.bot.token)
|
|
51
|
+
kwargs = dataclass_config_to_kwargs(webhook_config or WebhookConfig())
|
|
52
|
+
if self.security is not None:
|
|
53
|
+
secret_token = await self.security.secret_token(target)
|
|
54
|
+
if secret_token is not None:
|
|
55
|
+
kwargs["secret_token"] = secret_token
|
|
56
|
+
return await self.bot.set_webhook(url=await self.route.build_url(target=target), **kwargs)
|
|
57
|
+
|
|
58
|
+
async def _on_startup(self, app: AppT, *args, **kwargs) -> None: # noqa: ARG002
|
|
59
|
+
logger.info("Starting single-bot webhook engine for bot %s", self.bot.id)
|
|
60
|
+
lifecycle_data = self._build_lifecycle_data(app=app, bot=self.bot, **kwargs)
|
|
61
|
+
await self.dispatcher.emit_startup(**lifecycle_data)
|
|
62
|
+
|
|
63
|
+
async def _on_shutdown(self, app: AppT, *args, **kwargs) -> None: # noqa: ARG002
|
|
64
|
+
logger.info("Stopping single-bot webhook engine for bot %s", self.bot.id)
|
|
65
|
+
await self._task_tracker.close(timeout=self.shutdown_timeout)
|
|
66
|
+
|
|
67
|
+
lifecycle_data = self._build_lifecycle_data(app=app, bot=self.bot, **kwargs)
|
|
68
|
+
await self.dispatcher.emit_shutdown(**lifecycle_data)
|
|
69
|
+
|
|
70
|
+
if self.bot.session is not None:
|
|
71
|
+
await self.bot.session.close()
|