aiogram-webhook 1.1.0__tar.gz → 2.0.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.
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/PKG-INFO +3 -3
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/README.md +2 -2
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/pyproject.toml +1 -1
- aiogram_webhook-2.0.0/src/aiogram_webhook/__init__.py +21 -0
- aiogram_webhook-1.1.0/src/aiogram_webhook/adapters/aiohttp.py → aiogram_webhook-2.0.0/src/aiogram_webhook/adapters/aiohttp/adapter.py +24 -25
- aiogram_webhook-2.0.0/src/aiogram_webhook/adapters/aiohttp/mapping.py +15 -0
- aiogram_webhook-2.0.0/src/aiogram_webhook/adapters/base_adapter.py +86 -0
- aiogram_webhook-2.0.0/src/aiogram_webhook/adapters/base_mapping.py +38 -0
- aiogram_webhook-2.0.0/src/aiogram_webhook/adapters/fastapi/__init__.py +0 -0
- aiogram_webhook-2.0.0/src/aiogram_webhook/adapters/fastapi/adapter.py +49 -0
- aiogram_webhook-2.0.0/src/aiogram_webhook/adapters/fastapi/mapping.py +15 -0
- aiogram_webhook-2.0.0/src/aiogram_webhook/config/__init__.py +0 -0
- aiogram_webhook-2.0.0/src/aiogram_webhook/config/bot.py +12 -0
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/engines/base.py +18 -18
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/engines/simple.py +10 -10
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/engines/token.py +7 -6
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/routing/base.py +1 -1
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/routing/path.py +1 -1
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/routing/query.py +1 -1
- aiogram_webhook-2.0.0/src/aiogram_webhook/security/checks/__init__.py +0 -0
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/security/checks/check.py +1 -1
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/security/checks/ip.py +24 -29
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/security/secret_token.py +9 -5
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/security/security.py +2 -3
- aiogram_webhook-1.1.0/src/aiogram_webhook/__init__.py +0 -21
- aiogram_webhook-1.1.0/src/aiogram_webhook/adapters/base.py +0 -92
- aiogram_webhook-1.1.0/src/aiogram_webhook/adapters/fastapi.py +0 -43
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/adapters/__init__.py +0 -0
- {aiogram_webhook-1.1.0/src/aiogram_webhook/security/checks → aiogram_webhook-2.0.0/src/aiogram_webhook/adapters/aiohttp}/__init__.py +0 -0
- /aiogram_webhook-1.1.0/src/aiogram_webhook/config.py → /aiogram_webhook-2.0.0/src/aiogram_webhook/config/webhook.py +0 -0
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/engines/__init__.py +0 -0
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/routing/__init__.py +0 -0
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/routing/static.py +0 -0
- {aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/security/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: aiogram-webhook
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
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
5
|
Keywords: aiogram,telegram,webhook,fastapi,aiohttp,multibot
|
|
6
6
|
Author: m-xim
|
|
@@ -201,7 +201,7 @@ from aiogram import Dispatcher, Router
|
|
|
201
201
|
from aiogram.client.default import DefaultBotProperties
|
|
202
202
|
from aiogram.types import Message
|
|
203
203
|
from aiogram.filters import Command, CommandObject
|
|
204
|
-
from aiogram_webhook import TokenEngine, FastApiWebAdapter, WebhookConfig
|
|
204
|
+
from aiogram_webhook import TokenEngine, FastApiWebAdapter, WebhookConfig, BotConfig
|
|
205
205
|
from aiogram_webhook.routing import PathRouting
|
|
206
206
|
|
|
207
207
|
router = Router()
|
|
@@ -225,7 +225,7 @@ engine = TokenEngine(
|
|
|
225
225
|
dispatcher,
|
|
226
226
|
web_adapter=FastApiWebAdapter(),
|
|
227
227
|
routing=PathRouting(url="https://example.com/webhook/{bot_token}"),
|
|
228
|
-
|
|
228
|
+
bot_config=BotConfig(default=DefaultBotProperties(parse_mode="HTML")),
|
|
229
229
|
webhook_config=WebhookConfig(allowed_updates=["message", "callback_query"]),
|
|
230
230
|
# security=Security(...)
|
|
231
231
|
)
|
|
@@ -166,7 +166,7 @@ from aiogram import Dispatcher, Router
|
|
|
166
166
|
from aiogram.client.default import DefaultBotProperties
|
|
167
167
|
from aiogram.types import Message
|
|
168
168
|
from aiogram.filters import Command, CommandObject
|
|
169
|
-
from aiogram_webhook import TokenEngine, FastApiWebAdapter, WebhookConfig
|
|
169
|
+
from aiogram_webhook import TokenEngine, FastApiWebAdapter, WebhookConfig, BotConfig
|
|
170
170
|
from aiogram_webhook.routing import PathRouting
|
|
171
171
|
|
|
172
172
|
router = Router()
|
|
@@ -190,7 +190,7 @@ engine = TokenEngine(
|
|
|
190
190
|
dispatcher,
|
|
191
191
|
web_adapter=FastApiWebAdapter(),
|
|
192
192
|
routing=PathRouting(url="https://example.com/webhook/{bot_token}"),
|
|
193
|
-
|
|
193
|
+
bot_config=BotConfig(default=DefaultBotProperties(parse_mode="HTML")),
|
|
194
194
|
webhook_config=WebhookConfig(allowed_updates=["message", "callback_query"]),
|
|
195
195
|
# security=Security(...)
|
|
196
196
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aiogram-webhook"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.0"
|
|
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" }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from aiogram_webhook.config.bot import BotConfig
|
|
2
|
+
from aiogram_webhook.config.webhook import WebhookConfig
|
|
3
|
+
from aiogram_webhook.engines.simple import SimpleEngine
|
|
4
|
+
from aiogram_webhook.engines.token import TokenEngine
|
|
5
|
+
|
|
6
|
+
__all__ = ["BotConfig", "SimpleEngine", "TokenEngine", "WebhookConfig"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from aiogram_webhook.adapters.aiohttp.adapter import AiohttpWebAdapter # noqa: F401
|
|
11
|
+
|
|
12
|
+
__all__.insert(0, "AiohttpWebAdapter")
|
|
13
|
+
except ImportError:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from aiogram_webhook.adapters.fastapi.adapter import FastApiWebAdapter # noqa: F401
|
|
18
|
+
|
|
19
|
+
__all__.insert(1, "FastApiWebAdapter")
|
|
20
|
+
except ImportError:
|
|
21
|
+
pass
|
|
@@ -3,48 +3,44 @@ from typing import TYPE_CHECKING, Any, cast
|
|
|
3
3
|
from aiohttp.web import Application, Request
|
|
4
4
|
from aiohttp.web_response import Response, json_response
|
|
5
5
|
|
|
6
|
-
from aiogram_webhook.adapters.
|
|
7
|
-
from aiogram_webhook.
|
|
6
|
+
from aiogram_webhook.adapters.aiohttp.mapping import AiohttpHeadersMapping, AiohttpQueryMapping
|
|
7
|
+
from aiogram_webhook.adapters.base_adapter import BoundRequest, WebAdapter
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from asyncio import Transport
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class AiohttpBoundRequest(BoundRequest):
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
class AiohttpBoundRequest(BoundRequest[Request]):
|
|
14
|
+
def __init__(self, request: Request):
|
|
15
|
+
super().__init__(request)
|
|
16
|
+
self._headers = AiohttpHeadersMapping(self.request.headers)
|
|
17
|
+
self._query_params = AiohttpQueryMapping(self.request.query)
|
|
16
18
|
|
|
17
|
-
async def json(self)
|
|
19
|
+
async def json(self):
|
|
18
20
|
return await self.request.json()
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def query_param(self, name: str) -> Any | None:
|
|
24
|
-
return self.request.query.get(name)
|
|
25
|
-
|
|
26
|
-
def path_param(self, name: str) -> Any | None:
|
|
27
|
-
return self.request.match_info.get(name)
|
|
28
|
-
|
|
29
|
-
def ip(self) -> IPAddress | str | None:
|
|
22
|
+
@property
|
|
23
|
+
def client_ip(self):
|
|
30
24
|
if peer_name := cast("Transport", self.request.transport).get_extra_info("peername"):
|
|
31
25
|
return peer_name[0]
|
|
32
|
-
|
|
33
26
|
return None
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
@property
|
|
29
|
+
def headers(self) -> AiohttpHeadersMapping:
|
|
30
|
+
return self._headers
|
|
37
31
|
|
|
32
|
+
@property
|
|
33
|
+
def query_params(self) -> AiohttpQueryMapping:
|
|
34
|
+
return self._query_params
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
@property
|
|
37
|
+
def path_params(self):
|
|
38
|
+
return self.request.match_info
|
|
42
39
|
|
|
43
|
-
This adapter integrates with aiohttp to handle webhook requests.
|
|
44
|
-
"""
|
|
45
40
|
|
|
41
|
+
class AiohttpWebAdapter(WebAdapter):
|
|
46
42
|
def bind(self, request: Request) -> AiohttpBoundRequest:
|
|
47
|
-
return AiohttpBoundRequest(
|
|
43
|
+
return AiohttpBoundRequest(request=request)
|
|
48
44
|
|
|
49
45
|
def register(self, app: Application, path, handler, on_startup=None, on_shutdown=None) -> None:
|
|
50
46
|
async def endpoint(request: Request):
|
|
@@ -55,3 +51,6 @@ class AiohttpWebAdapter(WebAdapter):
|
|
|
55
51
|
app.on_startup.append(on_startup)
|
|
56
52
|
if on_shutdown is not None:
|
|
57
53
|
app.on_shutdown.append(on_shutdown)
|
|
54
|
+
|
|
55
|
+
def create_json_response(self, status: int, payload: dict[str, Any]) -> Response:
|
|
56
|
+
return json_response(status=status, data=payload)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from multidict import CIMultiDictProxy, MultiMapping
|
|
4
|
+
|
|
5
|
+
from aiogram_webhook.adapters.base_mapping import MappingABC
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AiohttpHeadersMapping(MappingABC[CIMultiDictProxy[str]]):
|
|
9
|
+
def getlist(self, name: str) -> list[Any]:
|
|
10
|
+
return self._mapping.getall(name, [])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AiohttpQueryMapping(MappingABC[MultiMapping[str]]):
|
|
14
|
+
def getlist(self, name: str) -> list[Any]:
|
|
15
|
+
return self._mapping.getall(name, [])
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
|
|
9
|
+
from aiogram_webhook.adapters.base_mapping import MappingABC
|
|
10
|
+
from aiogram_webhook.security.checks.ip import IPAddress
|
|
11
|
+
|
|
12
|
+
R = TypeVar("R")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BoundRequest(ABC, Generic[R]):
|
|
16
|
+
"""
|
|
17
|
+
Unified abstraction for requests across frameworks.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__slots__ = ("request",)
|
|
21
|
+
|
|
22
|
+
def __init__(self, request: R) -> None:
|
|
23
|
+
self.request = request
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def json(self) -> dict[str, Any]:
|
|
27
|
+
"""Get JSON data from request."""
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def client_ip(self) -> IPAddress | str | None:
|
|
33
|
+
"""Get client IP address."""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def headers(self) -> MappingABC:
|
|
39
|
+
"""Get request headers."""
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def query_params(self) -> MappingABC:
|
|
45
|
+
"""Get request query parameters."""
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def path_params(self) -> dict[str, Any]:
|
|
51
|
+
"""Get request path parameters."""
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class WebAdapter(ABC):
|
|
56
|
+
"""Abstraction for web framework adapters."""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def bind(self, request: Any) -> BoundRequest:
|
|
60
|
+
"""Bind request to BoundRequest."""
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def register(
|
|
65
|
+
self,
|
|
66
|
+
app: Any,
|
|
67
|
+
path: str,
|
|
68
|
+
handler: Callable[[BoundRequest], Awaitable[Any]],
|
|
69
|
+
on_startup: Callable[..., Awaitable[Any]] | None = None,
|
|
70
|
+
on_shutdown: Callable[..., Awaitable[Any]] | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Register webhook handler.
|
|
74
|
+
|
|
75
|
+
:param app: Web application instance.
|
|
76
|
+
:param path: Webhook path.
|
|
77
|
+
:param handler: Handler function.
|
|
78
|
+
:param on_startup: Optional startup callback.
|
|
79
|
+
:param on_shutdown: Optional shutdown callback.
|
|
80
|
+
"""
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def create_json_response(self, status: int, payload: dict[str, Any]) -> Any:
|
|
85
|
+
"""Create JSON response with given status and data."""
|
|
86
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import ItemsView, KeysView, Mapping, ValuesView
|
|
3
|
+
from typing import Any, Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
M = TypeVar("M", bound=Mapping)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MappingABC(ABC, Generic[M]):
|
|
9
|
+
def __init__(self, mapping: M):
|
|
10
|
+
self._mapping = mapping
|
|
11
|
+
|
|
12
|
+
def get(self, name: str, default=None):
|
|
13
|
+
return self._mapping.get(name, default)
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def getlist(self, name: str) -> list[Any]:
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def __getitem__(self, name: str) -> Any:
|
|
20
|
+
return self._mapping[name]
|
|
21
|
+
|
|
22
|
+
def __contains__(self, name: str) -> bool:
|
|
23
|
+
return name in self.keys()
|
|
24
|
+
|
|
25
|
+
def __len__(self) -> int:
|
|
26
|
+
return len(self._mapping)
|
|
27
|
+
|
|
28
|
+
def __iter__(self):
|
|
29
|
+
return iter(self._mapping)
|
|
30
|
+
|
|
31
|
+
def keys(self) -> KeysView:
|
|
32
|
+
return self._mapping.keys()
|
|
33
|
+
|
|
34
|
+
def values(self) -> ValuesView:
|
|
35
|
+
return self._mapping.values()
|
|
36
|
+
|
|
37
|
+
def items(self) -> ItemsView:
|
|
38
|
+
return self._mapping.items()
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, Request
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
from aiogram_webhook.adapters.base_adapter import BoundRequest, WebAdapter
|
|
7
|
+
from aiogram_webhook.adapters.fastapi.mapping import FastAPIHeadersMapping, FastAPIQueryMapping
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FastAPIBoundRequest(BoundRequest[Request]):
|
|
11
|
+
def __init__(self, request: Request):
|
|
12
|
+
super().__init__(request)
|
|
13
|
+
self._headers = FastAPIHeadersMapping(self.request.headers)
|
|
14
|
+
self._query_params = FastAPIQueryMapping(self.request.query_params)
|
|
15
|
+
|
|
16
|
+
async def json(self) -> dict[str, Any]:
|
|
17
|
+
return await self.request.json()
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def client_ip(self):
|
|
21
|
+
if self.request.client:
|
|
22
|
+
return self.request.client.host
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def headers(self) -> FastAPIHeadersMapping:
|
|
27
|
+
return self._headers
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def query_params(self) -> FastAPIQueryMapping:
|
|
31
|
+
return self._query_params
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def path_params(self):
|
|
35
|
+
return self.request.path_params
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FastApiWebAdapter(WebAdapter):
|
|
39
|
+
def bind(self, request: Request) -> FastAPIBoundRequest:
|
|
40
|
+
return FastAPIBoundRequest(request=request)
|
|
41
|
+
|
|
42
|
+
def register(self, app: FastAPI, path, handler, on_startup=None, on_shutdown=None) -> None: # noqa: ARG002
|
|
43
|
+
async def endpoint(request: Request):
|
|
44
|
+
return await handler(self.bind(request))
|
|
45
|
+
|
|
46
|
+
app.add_api_route(path=path, endpoint=endpoint, methods=["POST"])
|
|
47
|
+
|
|
48
|
+
def create_json_response(self, status: int, payload: dict[str, Any]) -> JSONResponse:
|
|
49
|
+
return JSONResponse(status_code=status, content=payload)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from starlette.datastructures import Headers, QueryParams
|
|
4
|
+
|
|
5
|
+
from aiogram_webhook.adapters.base_mapping import MappingABC
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FastAPIHeadersMapping(MappingABC[Headers]):
|
|
9
|
+
def getlist(self, name: str) -> list[Any]:
|
|
10
|
+
return self._mapping.getlist(name)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FastAPIQueryMapping(MappingABC[QueryParams]):
|
|
14
|
+
def getlist(self, name: str) -> list[Any]:
|
|
15
|
+
return self._mapping.getlist(name)
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from aiogram.client.default import DefaultBotProperties
|
|
2
|
+
from aiogram.client.session.base import BaseSession
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BotConfig(BaseModel):
|
|
7
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
8
|
+
|
|
9
|
+
session: BaseSession | None = None
|
|
10
|
+
"""HTTP Client session (For example AiohttpSession). If not specified it will be automatically created."""
|
|
11
|
+
default: DefaultBotProperties | None = None
|
|
12
|
+
"""Default bot properties. If specified it will be propagated into the API methods at runtime."""
|
|
@@ -6,14 +6,14 @@ from typing import TYPE_CHECKING, Any
|
|
|
6
6
|
|
|
7
7
|
from aiogram.methods import TelegramMethod
|
|
8
8
|
|
|
9
|
-
from aiogram_webhook.config import WebhookConfig
|
|
9
|
+
from aiogram_webhook.config.webhook import WebhookConfig
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from aiogram import Bot, Dispatcher
|
|
13
13
|
from aiogram.methods.base import TelegramType
|
|
14
14
|
from aiogram.types import InputFile
|
|
15
15
|
|
|
16
|
-
from aiogram_webhook.adapters.
|
|
16
|
+
from aiogram_webhook.adapters.base_adapter import BoundRequest, WebAdapter
|
|
17
17
|
from aiogram_webhook.routing.base import BaseRouting
|
|
18
18
|
from aiogram_webhook.security.security import Security
|
|
19
19
|
|
|
@@ -37,10 +37,10 @@ class WebhookEngine(ABC):
|
|
|
37
37
|
webhook_config: WebhookConfig | None = None,
|
|
38
38
|
handle_in_background: bool = True,
|
|
39
39
|
) -> None:
|
|
40
|
-
self.security = security
|
|
41
40
|
self.dispatcher = dispatcher
|
|
42
41
|
self.web_adapter = web_adapter
|
|
43
42
|
self.routing = routing
|
|
43
|
+
self.security = security
|
|
44
44
|
self.webhook_config = webhook_config or WebhookConfig()
|
|
45
45
|
self.handle_in_background = handle_in_background
|
|
46
46
|
self._background_feed_update_tasks: set[asyncio.Task[Any]] = set()
|
|
@@ -74,17 +74,17 @@ class WebhookEngine(ABC):
|
|
|
74
74
|
async def handle_request(self, bound_request: BoundRequest):
|
|
75
75
|
bot = self._get_bot_from_request(bound_request)
|
|
76
76
|
if bot is None:
|
|
77
|
-
return
|
|
77
|
+
return self.web_adapter.create_json_response(status=400, payload={"detail": "Bot not found"})
|
|
78
|
+
|
|
79
|
+
if self.security is not None and not await self.security.verify(bot=bot, bound_request=bound_request):
|
|
80
|
+
return self.web_adapter.create_json_response(status=403, payload={"detail": "Forbidden"})
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
is_allowed = await self.security.verify(bot=bot, bound_request=bound_request)
|
|
81
|
-
if not is_allowed:
|
|
82
|
-
return bound_request.json_response(status=403, payload={"detail": "Forbidden"})
|
|
82
|
+
update = await bound_request.json()
|
|
83
83
|
|
|
84
84
|
if self.handle_in_background:
|
|
85
|
-
return await self._handle_request_background(bot=bot,
|
|
85
|
+
return await self._handle_request_background(bot=bot, update=update)
|
|
86
86
|
|
|
87
|
-
return await self._handle_request(bot=bot,
|
|
87
|
+
return await self._handle_request(bot=bot, update=update)
|
|
88
88
|
|
|
89
89
|
def register(self, app: Any) -> None:
|
|
90
90
|
self.web_adapter.register(
|
|
@@ -95,33 +95,33 @@ class WebhookEngine(ABC):
|
|
|
95
95
|
on_shutdown=self.on_shutdown,
|
|
96
96
|
)
|
|
97
97
|
|
|
98
|
-
async def _handle_request(self, bot: Bot,
|
|
99
|
-
result = await self.dispatcher.feed_webhook_update(bot=bot, update=
|
|
98
|
+
async def _handle_request(self, bot: Bot, update: dict[str, Any]) -> dict[str, Any]:
|
|
99
|
+
result = await self.dispatcher.feed_webhook_update(bot=bot, update=update)
|
|
100
100
|
|
|
101
101
|
if not isinstance(result, TelegramMethod):
|
|
102
|
-
return
|
|
102
|
+
return self.web_adapter.create_json_response(status=200, payload={})
|
|
103
103
|
|
|
104
104
|
payload = self._build_webhook_payload(bot, result)
|
|
105
105
|
if payload is None:
|
|
106
106
|
# Has new files (InputFile) — execute directly via API
|
|
107
107
|
await self.dispatcher.silent_call_request(bot=bot, result=result)
|
|
108
|
-
return
|
|
108
|
+
return self.web_adapter.create_json_response(status=200, payload={})
|
|
109
109
|
|
|
110
|
-
return
|
|
110
|
+
return self.web_adapter.create_json_response(status=200, payload=payload)
|
|
111
111
|
|
|
112
112
|
async def _background_feed_update(self, bot: Bot, update: dict[str, Any]) -> None:
|
|
113
113
|
result = await self.dispatcher.feed_raw_update(bot=bot, update=update) # **self.data
|
|
114
114
|
if isinstance(result, TelegramMethod):
|
|
115
115
|
await self.dispatcher.silent_call_request(bot=bot, result=result)
|
|
116
116
|
|
|
117
|
-
async def _handle_request_background(self, bot: Bot,
|
|
117
|
+
async def _handle_request_background(self, bot: Bot, update: dict[str, Any]):
|
|
118
118
|
feed_update_task = asyncio.create_task(
|
|
119
|
-
self._background_feed_update(bot=bot, update=
|
|
119
|
+
self._background_feed_update(bot=bot, update=update),
|
|
120
120
|
)
|
|
121
121
|
self._background_feed_update_tasks.add(feed_update_task)
|
|
122
122
|
feed_update_task.add_done_callback(self._background_feed_update_tasks.discard)
|
|
123
123
|
|
|
124
|
-
return
|
|
124
|
+
return self.web_adapter.create_json_response(status=200, payload={})
|
|
125
125
|
|
|
126
126
|
@staticmethod
|
|
127
127
|
def _build_webhook_payload(bot: Bot, method: TelegramMethod[TelegramType]) -> dict[str, Any] | None:
|
|
@@ -7,8 +7,8 @@ from aiogram_webhook.engines.base import WebhookEngine
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from aiogram import Bot, Dispatcher
|
|
9
9
|
|
|
10
|
-
from aiogram_webhook.adapters.
|
|
11
|
-
from aiogram_webhook.config import WebhookConfig
|
|
10
|
+
from aiogram_webhook.adapters.base_adapter import BoundRequest, WebAdapter
|
|
11
|
+
from aiogram_webhook.config.webhook import WebhookConfig
|
|
12
12
|
from aiogram_webhook.routing.base import BaseRouting
|
|
13
13
|
from aiogram_webhook.security.security import Security
|
|
14
14
|
|
|
@@ -51,13 +51,6 @@ class SimpleEngine(WebhookEngine):
|
|
|
51
51
|
"""
|
|
52
52
|
return self.bot
|
|
53
53
|
|
|
54
|
-
async def on_startup(self, app: Any, *args, **kwargs) -> None: # noqa: ARG002
|
|
55
|
-
"""
|
|
56
|
-
Called on application startup. Emits dispatcher startup event.
|
|
57
|
-
"""
|
|
58
|
-
workflow_data = self._build_workflow_data(app=app, bot=self.bot, **kwargs)
|
|
59
|
-
await self.dispatcher.emit_startup(**workflow_data)
|
|
60
|
-
|
|
61
54
|
async def set_webhook(
|
|
62
55
|
self,
|
|
63
56
|
*,
|
|
@@ -84,7 +77,7 @@ class SimpleEngine(WebhookEngine):
|
|
|
84
77
|
)
|
|
85
78
|
params = config.model_dump(exclude_none=True)
|
|
86
79
|
|
|
87
|
-
if self.security:
|
|
80
|
+
if self.security is not None:
|
|
88
81
|
secret_token = await self.security.get_secret_token(bot=self.bot)
|
|
89
82
|
if secret_token is not None:
|
|
90
83
|
params["secret_token"] = secret_token
|
|
@@ -92,6 +85,13 @@ class SimpleEngine(WebhookEngine):
|
|
|
92
85
|
await self.bot.set_webhook(url=self.routing.webhook_point(self.bot), request_timeout=request_timeout, **params)
|
|
93
86
|
return self.bot
|
|
94
87
|
|
|
88
|
+
async def on_startup(self, app: Any, *args, **kwargs) -> None: # noqa: ARG002
|
|
89
|
+
"""
|
|
90
|
+
Called on application startup. Emits dispatcher startup event.
|
|
91
|
+
"""
|
|
92
|
+
workflow_data = self._build_workflow_data(app=app, bot=self.bot, **kwargs)
|
|
93
|
+
await self.dispatcher.emit_startup(**workflow_data)
|
|
94
|
+
|
|
95
95
|
async def on_shutdown(self, app: Any, *args, **kwargs) -> None: # noqa: ARG002
|
|
96
96
|
"""
|
|
97
97
|
Called on application shutdown. Emits dispatcher shutdown event and closes bot session.
|
|
@@ -5,11 +5,12 @@ from typing import TYPE_CHECKING, Any
|
|
|
5
5
|
from aiogram import Bot, Dispatcher
|
|
6
6
|
from aiogram.utils.token import extract_bot_id
|
|
7
7
|
|
|
8
|
+
from aiogram_webhook.config.bot import BotConfig
|
|
8
9
|
from aiogram_webhook.engines.base import WebhookEngine
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
|
-
from aiogram_webhook.adapters.
|
|
12
|
-
from aiogram_webhook.config import WebhookConfig
|
|
12
|
+
from aiogram_webhook.adapters.base_adapter import BoundRequest, WebAdapter
|
|
13
|
+
from aiogram_webhook.config.webhook import WebhookConfig
|
|
13
14
|
from aiogram_webhook.routing.base import TokenRouting
|
|
14
15
|
from aiogram_webhook.security.security import Security
|
|
15
16
|
|
|
@@ -29,7 +30,7 @@ class TokenEngine(WebhookEngine):
|
|
|
29
30
|
web_adapter: WebAdapter,
|
|
30
31
|
routing: TokenRouting,
|
|
31
32
|
security: Security | None = None,
|
|
32
|
-
|
|
33
|
+
bot_config: BotConfig | None = None,
|
|
33
34
|
webhook_config: WebhookConfig | None = None,
|
|
34
35
|
handle_in_background: bool = True,
|
|
35
36
|
) -> None:
|
|
@@ -42,7 +43,7 @@ class TokenEngine(WebhookEngine):
|
|
|
42
43
|
handle_in_background=handle_in_background,
|
|
43
44
|
)
|
|
44
45
|
self.routing: TokenRouting = routing # for type checker
|
|
45
|
-
self.
|
|
46
|
+
self.bot_config = bot_config or BotConfig()
|
|
46
47
|
self._bots: dict[int, Bot] = {}
|
|
47
48
|
|
|
48
49
|
def _get_bot_from_request(self, bound_request: BoundRequest) -> Bot | None:
|
|
@@ -70,7 +71,7 @@ class TokenEngine(WebhookEngine):
|
|
|
70
71
|
"""
|
|
71
72
|
bot = self._bots.get(extract_bot_id(token))
|
|
72
73
|
if not bot:
|
|
73
|
-
bot = Bot(token=token,
|
|
74
|
+
bot = Bot(token=token, session=self.bot_config.session, default=self.bot_config.default)
|
|
74
75
|
self._bots[bot.id] = bot
|
|
75
76
|
return bot
|
|
76
77
|
|
|
@@ -103,7 +104,7 @@ class TokenEngine(WebhookEngine):
|
|
|
103
104
|
)
|
|
104
105
|
params = config.model_dump(exclude_none=True)
|
|
105
106
|
|
|
106
|
-
if self.security:
|
|
107
|
+
if self.security is not None:
|
|
107
108
|
secret_token = await self.security.get_secret_token(bot=bot)
|
|
108
109
|
if secret_token is not None:
|
|
109
110
|
params["secret_token"] = secret_token
|
|
File without changes
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_address, ip_network
|
|
4
|
-
from typing import
|
|
2
|
+
from typing import Final
|
|
5
3
|
|
|
4
|
+
from aiogram_webhook.adapters.base_adapter import BoundRequest
|
|
6
5
|
from aiogram_webhook.security.checks.check import SecurityCheck
|
|
7
6
|
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from aiogram_webhook.adapters.base import BoundRequest
|
|
10
|
-
|
|
11
7
|
IPNetwork = IPv4Network | IPv6Network
|
|
12
8
|
IPAddress = IPv4Address | IPv6Address
|
|
13
9
|
|
|
@@ -47,29 +43,7 @@ class IPCheck(SecurityCheck):
|
|
|
47
43
|
else:
|
|
48
44
|
self._addresses.add(parsed)
|
|
49
45
|
|
|
50
|
-
def
|
|
51
|
-
"""
|
|
52
|
-
Extract client IP from X-Forwarded-For header.
|
|
53
|
-
|
|
54
|
-
Request got through multiple proxy/load balancers
|
|
55
|
-
https://github.com/aiogram/aiogram/issues/672
|
|
56
|
-
"""
|
|
57
|
-
header_value = bound_request.header("X-Forwarded-For")
|
|
58
|
-
if not header_value:
|
|
59
|
-
return None
|
|
60
|
-
forwarded_for, *_ = header_value.split(",", maxsplit=1)
|
|
61
|
-
return forwarded_for.strip()
|
|
62
|
-
|
|
63
|
-
def _get_client_ip(self, bound_request: BoundRequest) -> IPAddress | str | None:
|
|
64
|
-
"""Get client IP, first trying X-Forwarded-For header, then direct connection."""
|
|
65
|
-
# Try to resolve client IP over reverse proxy
|
|
66
|
-
if forwarded_for := self._extract_ip_from_x_forwarded_for(bound_request):
|
|
67
|
-
return forwarded_for
|
|
68
|
-
|
|
69
|
-
# Get direct IP from connection
|
|
70
|
-
return bound_request.ip()
|
|
71
|
-
|
|
72
|
-
async def verify(self, bot, bound_request) -> bool: # noqa: ARG002
|
|
46
|
+
async def verify(self, bot, bound_request: BoundRequest) -> bool: # noqa: ARG002
|
|
73
47
|
raw_ip = self._get_client_ip(bound_request)
|
|
74
48
|
if not raw_ip:
|
|
75
49
|
return False
|
|
@@ -79,6 +53,27 @@ class IPCheck(SecurityCheck):
|
|
|
79
53
|
return False
|
|
80
54
|
return (ip_addr in self._addresses) or any(ip_addr in network for network in self._networks)
|
|
81
55
|
|
|
56
|
+
def _get_client_ip(self, bound_request: BoundRequest) -> IPAddress | str | None:
|
|
57
|
+
# Try to resolve client IP over reverse proxy
|
|
58
|
+
# See: https://github.com/aiogram/aiogram/issues/672
|
|
59
|
+
if forwarded_for := self._extract_first_ip_from_header(bound_request.headers.get("X-Forwarded-For")):
|
|
60
|
+
return forwarded_for
|
|
61
|
+
|
|
62
|
+
# Get direct IP from connection
|
|
63
|
+
return bound_request.client_ip
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def _extract_first_ip_from_header(header_value: str | None) -> str | None:
|
|
67
|
+
"""
|
|
68
|
+
Extract the first IP from a comma-separated header value (e.g., X-Forwarded-For).
|
|
69
|
+
|
|
70
|
+
:param header_value: header value with possible IP chain
|
|
71
|
+
:return: first IP or None
|
|
72
|
+
"""
|
|
73
|
+
if header_value:
|
|
74
|
+
return header_value.split(",", maxsplit=1)[0].strip()
|
|
75
|
+
return None
|
|
76
|
+
|
|
82
77
|
@staticmethod
|
|
83
78
|
def _parse(item: IPAddress | IPNetwork | str) -> IPAddress | IPNetwork | None:
|
|
84
79
|
if isinstance(item, (IPNetwork, IPAddress)):
|
{aiogram_webhook-1.1.0 → aiogram_webhook-2.0.0}/src/aiogram_webhook/security/secret_token.py
RENAMED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
2
3
|
from hmac import compare_digest
|
|
3
|
-
from typing import Protocol
|
|
4
4
|
|
|
5
5
|
from aiogram import Bot
|
|
6
6
|
|
|
7
|
-
from aiogram_webhook.adapters.
|
|
7
|
+
from aiogram_webhook.adapters.base_adapter import BoundRequest
|
|
8
8
|
|
|
9
9
|
SECRET_TOKEN_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,256}$")
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class SecretToken(
|
|
12
|
+
class SecretToken(ABC):
|
|
13
13
|
"""
|
|
14
|
-
|
|
14
|
+
Abstract base class for secret token verification in webhook requests.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
secret_header: str = "x-telegram-bot-api-secret-token" # noqa: S105
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
17
20
|
async def verify(self, bot: Bot, bound_request: BoundRequest) -> bool:
|
|
18
21
|
"""
|
|
19
22
|
Verify the secret token in the incoming request.
|
|
20
23
|
"""
|
|
21
24
|
raise NotImplementedError
|
|
22
25
|
|
|
26
|
+
@abstractmethod
|
|
23
27
|
def secret_token(self, bot: Bot) -> str:
|
|
24
28
|
"""
|
|
25
29
|
Return the secret token for the given bot.
|
|
@@ -41,7 +45,7 @@ class StaticSecretToken(SecretToken):
|
|
|
41
45
|
self._token = token
|
|
42
46
|
|
|
43
47
|
async def verify(self, bot: Bot, bound_request: BoundRequest) -> bool: # noqa: ARG002
|
|
44
|
-
incoming = bound_request.
|
|
48
|
+
incoming = bound_request.headers.get(self.secret_header)
|
|
45
49
|
if incoming is None:
|
|
46
50
|
return False
|
|
47
51
|
return compare_digest(incoming, self._token)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from aiogram import Bot
|
|
2
2
|
|
|
3
|
-
from aiogram_webhook.adapters.
|
|
3
|
+
from aiogram_webhook.adapters.base_adapter import BoundRequest
|
|
4
4
|
from aiogram_webhook.security.checks.check import SecurityCheck
|
|
5
5
|
from aiogram_webhook.security.secret_token import SecretToken
|
|
6
6
|
|
|
@@ -28,8 +28,7 @@ class Security:
|
|
|
28
28
|
return False
|
|
29
29
|
|
|
30
30
|
for checker in self._checks:
|
|
31
|
-
|
|
32
|
-
if not ok:
|
|
31
|
+
if not await checker.verify(bot=bot, bound_request=bound_request):
|
|
33
32
|
return False
|
|
34
33
|
|
|
35
34
|
return True
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
from aiogram_webhook.adapters.base import WebAdapter
|
|
2
|
-
from aiogram_webhook.config import WebhookConfig
|
|
3
|
-
from aiogram_webhook.engines.simple import SimpleEngine
|
|
4
|
-
from aiogram_webhook.engines.token import TokenEngine
|
|
5
|
-
|
|
6
|
-
__all__ = ["SimpleEngine", "TokenEngine", "WebAdapter", "WebhookConfig"]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
try:
|
|
10
|
-
from aiogram_webhook.adapters.aiohttp import AiohttpWebAdapter # noqa: F401
|
|
11
|
-
|
|
12
|
-
__all__.insert(0, "AiohttpWebAdapter")
|
|
13
|
-
except ImportError:
|
|
14
|
-
pass
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
from aiogram_webhook.adapters.fastapi import FastApiWebAdapter # noqa: F401
|
|
18
|
-
|
|
19
|
-
__all__.insert(1, "FastApiWebAdapter")
|
|
20
|
-
except ImportError:
|
|
21
|
-
pass
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
-
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
from collections.abc import Awaitable, Callable
|
|
9
|
-
|
|
10
|
-
from aiogram_webhook.security.checks.ip import IPAddress
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@dataclass(slots=True)
|
|
14
|
-
class BoundRequest(ABC):
|
|
15
|
-
"""
|
|
16
|
-
Abstract base class for a request bound to a web adapter.
|
|
17
|
-
|
|
18
|
-
Provides interface for extracting data from incoming requests and generating responses.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
request: Any
|
|
22
|
-
adapter: WebAdapter
|
|
23
|
-
|
|
24
|
-
@abstractmethod
|
|
25
|
-
async def json(self) -> dict[str, Any]:
|
|
26
|
-
"""Parse the request body as JSON and return the resulting dictionary."""
|
|
27
|
-
raise NotImplementedError
|
|
28
|
-
|
|
29
|
-
@abstractmethod
|
|
30
|
-
def header(self, name: str) -> Any | None:
|
|
31
|
-
"""Get a header value from the request."""
|
|
32
|
-
raise NotImplementedError
|
|
33
|
-
|
|
34
|
-
@abstractmethod
|
|
35
|
-
def query_param(self, name: str) -> Any | None:
|
|
36
|
-
"""Get a query parameter from the request URL."""
|
|
37
|
-
raise NotImplementedError
|
|
38
|
-
|
|
39
|
-
@abstractmethod
|
|
40
|
-
def path_param(self, name: str) -> Any | None:
|
|
41
|
-
"""Get a path parameter from the request URL."""
|
|
42
|
-
raise NotImplementedError
|
|
43
|
-
|
|
44
|
-
@abstractmethod
|
|
45
|
-
def ip(self) -> IPAddress | str | None:
|
|
46
|
-
"""Get IP directly from client connection (implementation-specific)."""
|
|
47
|
-
raise NotImplementedError
|
|
48
|
-
|
|
49
|
-
def secret_token(self) -> str | None:
|
|
50
|
-
"""Get the secret token from the request header."""
|
|
51
|
-
return self.header(self.adapter.secret_header)
|
|
52
|
-
|
|
53
|
-
@abstractmethod
|
|
54
|
-
def json_response(self, status: int, payload: dict[str, Any]) -> Any:
|
|
55
|
-
"""Create a JSON response with the given status and payload."""
|
|
56
|
-
raise NotImplementedError
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@dataclass
|
|
60
|
-
class WebAdapter(ABC):
|
|
61
|
-
"""
|
|
62
|
-
Abstract base class for web framework adapters.
|
|
63
|
-
|
|
64
|
-
Provides interface for binding requests and registering webhook handlers.
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
secret_header: str = "x-telegram-bot-api-secret-token" # noqa: S105
|
|
68
|
-
|
|
69
|
-
@abstractmethod
|
|
70
|
-
def bind(self, request: Any) -> BoundRequest:
|
|
71
|
-
"""Bind a request to a BoundRequest instance."""
|
|
72
|
-
raise NotImplementedError
|
|
73
|
-
|
|
74
|
-
@abstractmethod
|
|
75
|
-
def register(
|
|
76
|
-
self,
|
|
77
|
-
app: Any,
|
|
78
|
-
path: str,
|
|
79
|
-
handler: Callable[[BoundRequest], Awaitable[Any]],
|
|
80
|
-
on_startup: Callable[..., Awaitable[Any]] | None = None,
|
|
81
|
-
on_shutdown: Callable[..., Awaitable[Any]] | None = None,
|
|
82
|
-
) -> None:
|
|
83
|
-
"""
|
|
84
|
-
Register a webhook handler with the adapter.
|
|
85
|
-
|
|
86
|
-
:param app: The web application instance.
|
|
87
|
-
:param path: The path for the webhook endpoint.
|
|
88
|
-
:param handler: The handler function to process incoming requests.
|
|
89
|
-
:param on_startup: Optional startup callback.
|
|
90
|
-
:param on_shutdown: Optional shutdown callback.
|
|
91
|
-
"""
|
|
92
|
-
raise NotImplementedError
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
from fastapi import FastAPI, Request
|
|
4
|
-
from fastapi.responses import JSONResponse
|
|
5
|
-
|
|
6
|
-
from aiogram_webhook.adapters.base import BoundRequest, WebAdapter
|
|
7
|
-
from aiogram_webhook.security.checks.ip import IPAddress
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class FastAPIBoundRequest(BoundRequest):
|
|
11
|
-
request: Request
|
|
12
|
-
|
|
13
|
-
async def json(self) -> dict[str, Any]:
|
|
14
|
-
return await self.request.json()
|
|
15
|
-
|
|
16
|
-
def header(self, name: str) -> Any | None:
|
|
17
|
-
return self.request.headers.get(name)
|
|
18
|
-
|
|
19
|
-
def query_param(self, name: str) -> Any | None:
|
|
20
|
-
return self.request.query_params.get(name)
|
|
21
|
-
|
|
22
|
-
def path_param(self, name: str) -> Any | None:
|
|
23
|
-
return self.request.path_params.get(name)
|
|
24
|
-
|
|
25
|
-
def ip(self) -> IPAddress | str | None:
|
|
26
|
-
if self.request.client:
|
|
27
|
-
return self.request.client.host
|
|
28
|
-
|
|
29
|
-
return None
|
|
30
|
-
|
|
31
|
-
def json_response(self, status: int, payload: dict[str, Any]) -> JSONResponse:
|
|
32
|
-
return JSONResponse(status_code=status, content=payload)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class FastApiWebAdapter(WebAdapter):
|
|
36
|
-
def bind(self, request: Request) -> FastAPIBoundRequest:
|
|
37
|
-
return FastAPIBoundRequest(adapter=self, request=request)
|
|
38
|
-
|
|
39
|
-
def register(self, app: FastAPI, path, handler, on_startup=None, on_shutdown=None) -> None: # noqa: ARG002
|
|
40
|
-
async def endpoint(request: Request):
|
|
41
|
-
return await handler(self.bind(request))
|
|
42
|
-
|
|
43
|
-
app.add_api_route(path=path, endpoint=endpoint, methods=["POST"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|