aiogram-webhook 0.0.1__py3-none-any.whl
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/__init__.py +12 -0
- aiogram_webhook/adapters/__init__.py +0 -0
- aiogram_webhook/adapters/base.py +67 -0
- aiogram_webhook/adapters/fastapi.py +41 -0
- aiogram_webhook/engines/__init__.py +5 -0
- aiogram_webhook/engines/base.py +129 -0
- aiogram_webhook/engines/simple.py +106 -0
- aiogram_webhook/engines/token.py +99 -0
- aiogram_webhook/routing/__init__.py +5 -0
- aiogram_webhook/routing/base.py +33 -0
- aiogram_webhook/routing/path.py +28 -0
- aiogram_webhook/routing/query.py +35 -0
- aiogram_webhook/security/__init__.py +6 -0
- aiogram_webhook/security/checks/__init__.py +0 -0
- aiogram_webhook/security/checks/check.py +14 -0
- aiogram_webhook/security/checks/ip.py +67 -0
- aiogram_webhook/security/secret_token.py +42 -0
- aiogram_webhook/security/security.py +38 -0
- aiogram_webhook-0.0.1.dist-info/METADATA +143 -0
- aiogram_webhook-0.0.1.dist-info/RECORD +22 -0
- aiogram_webhook-0.0.1.dist-info/WHEEL +4 -0
- aiogram_webhook-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from aiogram_webhook.adapters.base import WebAdapter
|
|
2
|
+
from aiogram_webhook.engines.simple import SimpleEngine
|
|
3
|
+
from aiogram_webhook.engines.token import TokenEngine
|
|
4
|
+
|
|
5
|
+
__all__ = ["SimpleEngine", "TokenEngine", "WebAdapter"]
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from aiogram_webhook.adapters.fastapi import FastApiWebAdapter # noqa: F401
|
|
9
|
+
|
|
10
|
+
__all__.insert(0, "FastApiWebAdapter")
|
|
11
|
+
except ImportError:
|
|
12
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
from ipaddress import IPv4Address, IPv6Address
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class BoundRequest(ABC):
|
|
14
|
+
"""
|
|
15
|
+
Abstract base class for a request bound to a web adapter.
|
|
16
|
+
|
|
17
|
+
Provides interface for extracting data from incoming requests and generating responses.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
request: Any
|
|
21
|
+
adapter: WebAdapter
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def json(self) -> dict[str, Any]:
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def header(self, name: str) -> Any | None:
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def query_param(self, name: str) -> Any | None:
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def path_param(self, name: str) -> Any | None:
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def ip(self) -> IPv4Address | IPv6Address | str | None:
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
def secret_token(self) -> str | None:
|
|
44
|
+
return self.header(self.adapter.secret_header)
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def json_response(self, status: int, payload: dict[str, Any]) -> Any:
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class WebAdapter(ABC):
|
|
53
|
+
"""
|
|
54
|
+
Abstract base class for web framework adapters.
|
|
55
|
+
|
|
56
|
+
Provides interface for binding requests and registering webhook handlers.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
secret_header: str = "x-telegram-bot-api-secret-token" # noqa: S105
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def bind(self, request: Any) -> BoundRequest:
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def register(self, app: Any, path: str, handler: Callable[[BoundRequest], Awaitable[Any]]) -> None:
|
|
67
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
from ipaddress import IPv4Address, IPv6Address
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi import Request
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
|
|
8
|
+
from aiogram_webhook.adapters.base import BoundRequest, WebAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FastAPIBoundRequest(BoundRequest):
|
|
12
|
+
request: Request
|
|
13
|
+
|
|
14
|
+
async def json(self) -> dict[str, Any]:
|
|
15
|
+
return await self.request.json()
|
|
16
|
+
|
|
17
|
+
def header(self, name: str) -> Any | None:
|
|
18
|
+
return self.request.headers.get(name)
|
|
19
|
+
|
|
20
|
+
def query_param(self, name: str) -> Any | None:
|
|
21
|
+
return self.request.query_params.get(name)
|
|
22
|
+
|
|
23
|
+
def path_param(self, name: str) -> Any | None:
|
|
24
|
+
return self.request.path_params.get(name)
|
|
25
|
+
|
|
26
|
+
def ip(self) -> IPv4Address | IPv6Address | str | None:
|
|
27
|
+
return self.request.client.host if self.request.client else None
|
|
28
|
+
|
|
29
|
+
def json_response(self, status: int, payload: dict[str, Any]) -> JSONResponse:
|
|
30
|
+
return JSONResponse(status_code=status, content=payload)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FastApiWebAdapter(WebAdapter):
|
|
34
|
+
def bind(self, request: Request) -> BoundRequest:
|
|
35
|
+
return FastAPIBoundRequest(adapter=self, request=request)
|
|
36
|
+
|
|
37
|
+
def register(self, app: Any, path: str, handler: Callable[[BoundRequest], Awaitable[Any]]) -> None:
|
|
38
|
+
async def endpoint(request: Request):
|
|
39
|
+
return await handler(self.bind(request))
|
|
40
|
+
|
|
41
|
+
app.add_api_route(path, endpoint, methods=["POST"])
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from aiogram import Bot, Dispatcher
|
|
7
|
+
from aiogram.methods import TelegramMethod
|
|
8
|
+
from aiogram.methods.base import TelegramType
|
|
9
|
+
|
|
10
|
+
from aiogram_webhook.adapters.base import BoundRequest, WebAdapter
|
|
11
|
+
from aiogram_webhook.routing.base import BaseRouting
|
|
12
|
+
from aiogram_webhook.security.checks.ip import IPCheck
|
|
13
|
+
from aiogram_webhook.security.security import Security
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from aiogram.types import InputFile
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WebhookEngine(ABC):
|
|
20
|
+
"""
|
|
21
|
+
Base webhook engine for processing Telegram bot updates.
|
|
22
|
+
|
|
23
|
+
Handles incoming webhook requests, bot resolution, security checks,
|
|
24
|
+
routing, and dispatching updates to the aiogram dispatcher. Supports
|
|
25
|
+
both synchronous and background processing.
|
|
26
|
+
|
|
27
|
+
Constructor arguments:
|
|
28
|
+
dispatcher: aiogram.Dispatcher instance for update processing.
|
|
29
|
+
web_adapter: Web framework adapter class.
|
|
30
|
+
routing: Webhook routing strategy.
|
|
31
|
+
security: True — protect by IP (IPCheck), False/None — not protect, Security — custom protect.
|
|
32
|
+
handle_in_background: Whether to process updates in background (default: True).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
dispatcher: Dispatcher,
|
|
38
|
+
/,
|
|
39
|
+
web_adapter: WebAdapter,
|
|
40
|
+
routing: BaseRouting,
|
|
41
|
+
security: Security | bool | None = None,
|
|
42
|
+
handle_in_background: bool = True,
|
|
43
|
+
) -> None:
|
|
44
|
+
if security is True:
|
|
45
|
+
self.security = Security(IPCheck())
|
|
46
|
+
else:
|
|
47
|
+
self.security = security
|
|
48
|
+
self.dispatcher = dispatcher
|
|
49
|
+
self.web_adapter = web_adapter
|
|
50
|
+
self.routing = routing
|
|
51
|
+
self.handle_in_background = handle_in_background
|
|
52
|
+
self._background_feed_update_tasks: set[asyncio.Task[Any]] = set()
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def resolve_bot_from_request(self, bound_request: BoundRequest) -> Bot | None:
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def on_startup(self, bots: Iterable[Bot] | None = None, **kwargs: Any) -> None:
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
async def on_shutdown(self) -> None:
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
async def handle_request(self, bound_request: BoundRequest):
|
|
67
|
+
bot = self.resolve_bot_from_request(bound_request)
|
|
68
|
+
if bot is None:
|
|
69
|
+
return bound_request.json_response(status=400, payload={"detail": "Bot not found"})
|
|
70
|
+
|
|
71
|
+
if self.security:
|
|
72
|
+
is_allowed = await self.security.verify(bot=bot, bound_request=bound_request)
|
|
73
|
+
if not is_allowed:
|
|
74
|
+
return bound_request.json_response(status=403, payload={"detail": "Forbidden"})
|
|
75
|
+
|
|
76
|
+
if self.handle_in_background:
|
|
77
|
+
return await self._handle_request_background(bot=bot, bound_request=bound_request)
|
|
78
|
+
|
|
79
|
+
return await self._handle_request(bot=bot, bound_request=bound_request)
|
|
80
|
+
|
|
81
|
+
def register(self, app: Any) -> None:
|
|
82
|
+
self.web_adapter.register(app=app, path=self.routing.path, handler=self.handle_request)
|
|
83
|
+
|
|
84
|
+
async def _handle_request(self, bot: Bot, bound_request: BoundRequest) -> dict[str, Any]:
|
|
85
|
+
result = await self.dispatcher.feed_webhook_update(bot=bot, update=await bound_request.json())
|
|
86
|
+
|
|
87
|
+
if not isinstance(result, TelegramMethod):
|
|
88
|
+
return bound_request.json_response(status=200, payload={})
|
|
89
|
+
|
|
90
|
+
if self._has_files(bot, result):
|
|
91
|
+
await self.dispatcher.silent_call_request(bot=bot, result=result)
|
|
92
|
+
return bound_request.json_response(status=200, payload={})
|
|
93
|
+
|
|
94
|
+
payload = self._to_webhook_json(bot, result)
|
|
95
|
+
return bound_request.json_response(status=200, payload=payload)
|
|
96
|
+
|
|
97
|
+
async def _background_feed_update(self, bot: Bot, update: dict[str, Any]) -> None:
|
|
98
|
+
result = await self.dispatcher.feed_raw_update(bot=bot, update=update) # **self.data
|
|
99
|
+
if isinstance(result, TelegramMethod):
|
|
100
|
+
await self.dispatcher.silent_call_request(bot=bot, result=result)
|
|
101
|
+
|
|
102
|
+
async def _handle_request_background(self, bot: Bot, bound_request: BoundRequest):
|
|
103
|
+
feed_update_task = asyncio.create_task(
|
|
104
|
+
self._background_feed_update(
|
|
105
|
+
bot=bot,
|
|
106
|
+
update=await bound_request.json(),
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
self._background_feed_update_tasks.add(feed_update_task)
|
|
110
|
+
feed_update_task.add_done_callback(self._background_feed_update_tasks.discard)
|
|
111
|
+
|
|
112
|
+
return bound_request.json_response(status=200, payload={})
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _has_files(bot: Bot, method: TelegramMethod[TelegramType]) -> bool:
|
|
116
|
+
files: dict[str, InputFile] = {}
|
|
117
|
+
for v in method.model_dump(warnings=False).values():
|
|
118
|
+
bot.session.prepare_value(v, bot=bot, files=files)
|
|
119
|
+
return bool(files)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _to_webhook_json(bot: Bot, method: TelegramMethod[TelegramType]) -> dict[str, Any]:
|
|
123
|
+
files: dict[str, InputFile] = {}
|
|
124
|
+
params: dict[str, Any] = {}
|
|
125
|
+
for k, v in method.model_dump(warnings=False).items():
|
|
126
|
+
pv = bot.session.prepare_value(v, bot=bot, files=files)
|
|
127
|
+
if pv is not None:
|
|
128
|
+
params[k] = pv
|
|
129
|
+
return {"method": method.__api_method__, **params}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from aiogram_webhook.engines.base import WebhookEngine
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
|
|
10
|
+
from aiogram import Bot, Dispatcher
|
|
11
|
+
|
|
12
|
+
from aiogram_webhook.adapters.base import BoundRequest, WebAdapter
|
|
13
|
+
from aiogram_webhook.routing.base import BaseRouting
|
|
14
|
+
from aiogram_webhook.security.security import Security
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SimpleEngine(WebhookEngine):
|
|
18
|
+
"""
|
|
19
|
+
Simple webhook engine for single-bot applications.
|
|
20
|
+
|
|
21
|
+
Uses a single Bot instance for all webhook requests.
|
|
22
|
+
Ideal for applications that handle only one bot.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
bot: The Bot instance to use for all requests.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
dispatcher: Dispatcher,
|
|
31
|
+
bot: Bot,
|
|
32
|
+
/,
|
|
33
|
+
web_adapter: WebAdapter,
|
|
34
|
+
routing: BaseRouting,
|
|
35
|
+
security: Security | bool | None = None,
|
|
36
|
+
handle_in_background: bool = True,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Initialize the SimpleEngine for one bot.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
dispatcher: Dispatcher instance for update processing.
|
|
43
|
+
bot: The Bot instance to use for all requests.
|
|
44
|
+
web_adapter: Web framework adapter class.
|
|
45
|
+
routing: Webhook routing strategy.
|
|
46
|
+
security: Security settings and checks.
|
|
47
|
+
handle_in_background: Whether to process updates in background.
|
|
48
|
+
"""
|
|
49
|
+
self.bot = bot
|
|
50
|
+
super().__init__(
|
|
51
|
+
dispatcher,
|
|
52
|
+
web_adapter=web_adapter,
|
|
53
|
+
routing=routing,
|
|
54
|
+
security=security,
|
|
55
|
+
handle_in_background=handle_in_background,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def resolve_bot_from_request(self, bound_request: BoundRequest) -> Bot | None: # noqa: ARG002
|
|
59
|
+
"""
|
|
60
|
+
Always returns the single Bot instance for any request.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
bound_request: The incoming bound request.
|
|
64
|
+
Returns:
|
|
65
|
+
The single Bot instance.
|
|
66
|
+
"""
|
|
67
|
+
return self.bot
|
|
68
|
+
|
|
69
|
+
async def on_startup(self, bots: Iterable[Bot] | None = None, **kwargs: Any) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Called on application startup. Emits dispatcher startup event for all bots.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
bots: Optional iterable of Bot instances.
|
|
75
|
+
**kwargs: Additional keyword arguments for dispatcher.
|
|
76
|
+
"""
|
|
77
|
+
all_bots = set(bots) | {self.bot} if bots else {self.bot}
|
|
78
|
+
await self.dispatcher.emit_startup(
|
|
79
|
+
dispatcher=self.dispatcher,
|
|
80
|
+
bots=all_bots,
|
|
81
|
+
webhook_engine=self,
|
|
82
|
+
**kwargs,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async def on_shutdown(self) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Called on application shutdown. Emits dispatcher shutdown event and closes bot session.
|
|
88
|
+
"""
|
|
89
|
+
await self.dispatcher.emit_shutdown(
|
|
90
|
+
dispatcher=self.dispatcher,
|
|
91
|
+
bots={self.bot},
|
|
92
|
+
webhook_engine=self,
|
|
93
|
+
)
|
|
94
|
+
await self.bot.session.close()
|
|
95
|
+
|
|
96
|
+
async def set_webhook(self, **kwargs) -> Bot:
|
|
97
|
+
"""
|
|
98
|
+
Sets the webhook for the single Bot instance.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
**kwargs: Additional arguments for set_webhook.
|
|
102
|
+
Returns:
|
|
103
|
+
The Bot instance after setting webhook.
|
|
104
|
+
"""
|
|
105
|
+
await self.bot.set_webhook(url=self.routing.webhook_point(self.bot), secret_token=None, **kwargs)
|
|
106
|
+
return self.bot
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from aiogram import Bot, Dispatcher
|
|
6
|
+
from aiogram.utils.token import extract_bot_id
|
|
7
|
+
|
|
8
|
+
from aiogram_webhook.engines.base import WebhookEngine
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
|
|
13
|
+
from aiogram_webhook.adapters.base import BoundRequest, WebAdapter
|
|
14
|
+
from aiogram_webhook.routing.base import BaseRouting
|
|
15
|
+
from aiogram_webhook.security.security import Security
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TokenEngine(WebhookEngine):
|
|
19
|
+
"""
|
|
20
|
+
Multi-bot webhook engine with dynamic bot resolution.
|
|
21
|
+
|
|
22
|
+
Resolves Bot instances from request tokens.
|
|
23
|
+
Creates and caches Bot instances on-demand. Suitable for multi-tenant applications.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
dispatcher: Dispatcher,
|
|
29
|
+
/,
|
|
30
|
+
web_adapter: WebAdapter,
|
|
31
|
+
routing: BaseRouting,
|
|
32
|
+
security: Security | bool | None = None,
|
|
33
|
+
bot_settings: dict[str, Any] | None = None,
|
|
34
|
+
handle_in_background: bool = True,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Initialize the TokenEngine for multi-bot applications.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
dispatcher: Dispatcher instance for update processing.
|
|
41
|
+
web_adapter: Web framework adapter class.
|
|
42
|
+
routing: Webhook routing strategy.
|
|
43
|
+
security: Security settings and checks.
|
|
44
|
+
bot_settings: Default settings for creating Bot instances.
|
|
45
|
+
handle_in_background: Whether to process updates in background.
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(
|
|
48
|
+
dispatcher,
|
|
49
|
+
web_adapter=web_adapter,
|
|
50
|
+
routing=routing,
|
|
51
|
+
security=security,
|
|
52
|
+
handle_in_background=handle_in_background,
|
|
53
|
+
)
|
|
54
|
+
self.bot_settings = bot_settings
|
|
55
|
+
self._bots: dict[int, Bot] = {}
|
|
56
|
+
|
|
57
|
+
def resolve_bot_from_request(self, bound_request: BoundRequest) -> Bot | None:
|
|
58
|
+
token = self.routing.extract_key(bound_request)
|
|
59
|
+
if not token:
|
|
60
|
+
return None
|
|
61
|
+
return self.resolve_bot(token)
|
|
62
|
+
|
|
63
|
+
async def on_startup(self, bots: Iterable[Bot] | None = None, **kwargs: Any) -> None:
|
|
64
|
+
"""Called on application startup. Emits dispatcher startup event for all bots."""
|
|
65
|
+
all_bots = set(bots) | set(self._bots.values()) if bots else set(self._bots.values())
|
|
66
|
+
|
|
67
|
+
await self.dispatcher.emit_startup(
|
|
68
|
+
dispatcher=self.dispatcher,
|
|
69
|
+
bots=all_bots,
|
|
70
|
+
webhook_engine=self,
|
|
71
|
+
**kwargs,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
async def on_shutdown(self) -> None:
|
|
75
|
+
"""Called on application shutdown. Emits dispatcher shutdown event and closes all bot sessions."""
|
|
76
|
+
await self.dispatcher.emit_shutdown(
|
|
77
|
+
dispatcher=self.dispatcher,
|
|
78
|
+
bots=set(self._bots.values()),
|
|
79
|
+
webhook_engine=self,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
for bot in self._bots.values():
|
|
83
|
+
await bot.session.close()
|
|
84
|
+
# self._bots.clear()
|
|
85
|
+
|
|
86
|
+
async def set_webhook(self, token: str, **kwargs) -> Bot:
|
|
87
|
+
"""Sets the webhook for the Bot instance resolved by token."""
|
|
88
|
+
bot = self.resolve_bot(token)
|
|
89
|
+
|
|
90
|
+
await bot.set_webhook(url=self.routing.webhook_point(bot), secret_token=None, **kwargs)
|
|
91
|
+
return bot
|
|
92
|
+
|
|
93
|
+
def resolve_bot(self, token: str) -> Bot:
|
|
94
|
+
"""Resolve or create a Bot instance by token and cache it."""
|
|
95
|
+
bot = self._bots.get(extract_bot_id(token))
|
|
96
|
+
if not bot:
|
|
97
|
+
bot = Bot(token=token, **(self.bot_settings or {}))
|
|
98
|
+
self._bots[bot.id] = bot
|
|
99
|
+
return bot
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from aiogram import Bot
|
|
4
|
+
from yarl import URL
|
|
5
|
+
|
|
6
|
+
from aiogram_webhook.adapters.base import BoundRequest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseRouting(ABC):
|
|
10
|
+
"""
|
|
11
|
+
Abstract base class for webhook routing strategies.
|
|
12
|
+
|
|
13
|
+
Defines how webhook URLs are constructed and how keys (tokens)
|
|
14
|
+
are extracted from incoming requests.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
url: Url template.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, url: str) -> None:
|
|
21
|
+
self.url = URL(url)
|
|
22
|
+
self.base = self.url.origin()
|
|
23
|
+
self.path = self.url.path
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def webhook_point(self, bot: Bot) -> str:
|
|
27
|
+
"""Return the webhook URL for the given bot."""
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def extract_key(self, bound_request: BoundRequest) -> str | None:
|
|
32
|
+
"""Extract the routing key (e.g., token) from the incoming request."""
|
|
33
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from aiogram import Bot
|
|
2
|
+
|
|
3
|
+
from aiogram_webhook.adapters.base import BoundRequest
|
|
4
|
+
from aiogram_webhook.routing.base import BaseRouting
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PathRouting(BaseRouting):
|
|
8
|
+
"""
|
|
9
|
+
URL path-based routing strategy.
|
|
10
|
+
|
|
11
|
+
Extracts bot token from URL path parameters.
|
|
12
|
+
Example: /webhook/{token} -> extracts token from path.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, url: str, param: str | None = None) -> None:
|
|
16
|
+
super().__init__(url=url)
|
|
17
|
+
self.param = param
|
|
18
|
+
|
|
19
|
+
def webhook_point(self, bot: Bot) -> str:
|
|
20
|
+
url = self.url.human_repr()
|
|
21
|
+
if self.param is None:
|
|
22
|
+
return url
|
|
23
|
+
return url.format_map({self.param: bot.token})
|
|
24
|
+
|
|
25
|
+
def extract_key(self, bound_request: BoundRequest) -> str | None:
|
|
26
|
+
if self.param is None:
|
|
27
|
+
return None
|
|
28
|
+
return bound_request.path_param(self.param)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from aiogram import Bot
|
|
2
|
+
|
|
3
|
+
from aiogram_webhook.adapters.base import BoundRequest
|
|
4
|
+
from aiogram_webhook.routing.base import BaseRouting
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class QueryRouting(BaseRouting):
|
|
8
|
+
"""
|
|
9
|
+
URL query parameter-based routing strategy.
|
|
10
|
+
|
|
11
|
+
Extracts bot token from query parameters.
|
|
12
|
+
Example: /webhook?token=123:ABC -> extracts token from query.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, url: str, param: str) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Initialize the query parameter-based routing strategy.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
url: The URL template for webhook endpoints.
|
|
21
|
+
param: The query parameter name for the bot token.
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(url=url)
|
|
24
|
+
self.param = param
|
|
25
|
+
|
|
26
|
+
def webhook_point(self, bot: Bot) -> str:
|
|
27
|
+
url = self.url.human_repr()
|
|
28
|
+
if self.param is None:
|
|
29
|
+
return url
|
|
30
|
+
return url.format_map({self.param: bot.token})
|
|
31
|
+
|
|
32
|
+
def extract_key(self, bound_request: BoundRequest) -> str | None:
|
|
33
|
+
if self.param is None:
|
|
34
|
+
return None
|
|
35
|
+
return bound_request.query_param(self.param)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from aiogram_webhook.security.checks.check import Check
|
|
2
|
+
from aiogram_webhook.security.checks.ip import IPCheck
|
|
3
|
+
from aiogram_webhook.security.secret_token import SecretToken, StaticSecretToken
|
|
4
|
+
from aiogram_webhook.security.security import Security
|
|
5
|
+
|
|
6
|
+
__all__ = ("Check", "IPCheck", "SecretToken", "Security", "StaticSecretToken")
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from aiogram import Bot
|
|
4
|
+
|
|
5
|
+
from aiogram_webhook.adapters.base import BoundRequest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Check(Protocol):
|
|
9
|
+
"""
|
|
10
|
+
Protocol for security check on webhook requests.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
async def verify(self, bot: Bot, bound_request: BoundRequest) -> bool:
|
|
14
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_address, ip_network
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
from aiogram_webhook.security.checks.check import Check
|
|
7
|
+
|
|
8
|
+
DEFAULT_TELEGRAM_NETWORKS: Final[tuple[IPv4Network | IPv6Network, ...]] = (
|
|
9
|
+
IPv4Network("149.154.160.0/20"),
|
|
10
|
+
IPv4Network("91.108.4.0/22"),
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
IPAddressOrNetwork = IPv4Network | IPv6Network | IPv4Address | IPv6Address
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class IPCheck(Check):
|
|
17
|
+
"""
|
|
18
|
+
Security check for validating client IP address against allowed networks and addresses.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *ip_entries: IPAddressOrNetwork | str, include_default: bool = True) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Initialize the IPCheck with allowed IP addresses and networks.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
*ip_entries: IP addresses or networks to allow.
|
|
27
|
+
include_default: Whether to include default Telegram IP networks.
|
|
28
|
+
"""
|
|
29
|
+
networks: set[IPv4Network | IPv6Network] = set()
|
|
30
|
+
addresses: set[IPv4Address | IPv6Address] = set()
|
|
31
|
+
|
|
32
|
+
if include_default:
|
|
33
|
+
networks.update(DEFAULT_TELEGRAM_NETWORKS)
|
|
34
|
+
|
|
35
|
+
for item in ip_entries:
|
|
36
|
+
parsed = self._parse(item)
|
|
37
|
+
if parsed is None:
|
|
38
|
+
continue
|
|
39
|
+
if isinstance(parsed, (IPv4Network, IPv6Network)):
|
|
40
|
+
networks.add(parsed)
|
|
41
|
+
else:
|
|
42
|
+
addresses.add(parsed)
|
|
43
|
+
|
|
44
|
+
self._networks: set[IPv4Network | IPv6Network] = networks
|
|
45
|
+
self._addresses: set[IPv4Address | IPv6Address] = addresses
|
|
46
|
+
|
|
47
|
+
async def verify(self, bot, bound_request) -> bool: # noqa: ARG002
|
|
48
|
+
raw_ip = bound_request.ip()
|
|
49
|
+
if not raw_ip:
|
|
50
|
+
return False
|
|
51
|
+
try:
|
|
52
|
+
addr = ip_address(raw_ip)
|
|
53
|
+
except ValueError:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
if addr in self._addresses:
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
return any(addr in net for net in self._networks)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def _parse(item: IPAddressOrNetwork | str) -> IPAddressOrNetwork | None:
|
|
63
|
+
if isinstance(item, (IPv4Network, IPv6Network, IPv4Address, IPv6Address)):
|
|
64
|
+
return item
|
|
65
|
+
if isinstance(item, str):
|
|
66
|
+
return ip_network(item, strict=False) if "/" in item else ip_address(item)
|
|
67
|
+
return None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from hmac import compare_digest
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
from aiogram import Bot
|
|
5
|
+
|
|
6
|
+
from aiogram_webhook.adapters.base import BoundRequest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SecretToken(Protocol):
|
|
10
|
+
"""
|
|
11
|
+
Protocol for secret token verification in webhook requests.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
async def verify(self, bot: Bot, bound_request: BoundRequest) -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Verify the secret token in the incoming request.
|
|
17
|
+
"""
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
def secret_token(self, bot: Bot) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Return the secret token for the given bot.
|
|
23
|
+
"""
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StaticSecretToken(SecretToken):
|
|
28
|
+
"""
|
|
29
|
+
Static secret token implementation for webhook security.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, token: str) -> None:
|
|
33
|
+
self._token = token
|
|
34
|
+
|
|
35
|
+
async def verify(self, bot: Bot, bound_request: BoundRequest) -> bool: # noqa: ARG002
|
|
36
|
+
incoming = bound_request.secret_token()
|
|
37
|
+
if incoming is None:
|
|
38
|
+
return False
|
|
39
|
+
return compare_digest(incoming, self._token)
|
|
40
|
+
|
|
41
|
+
def secret_token(self, bot: Bot) -> str: # noqa: ARG002
|
|
42
|
+
return self._token
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from aiogram import Bot
|
|
2
|
+
|
|
3
|
+
from aiogram_webhook.adapters.base import BoundRequest
|
|
4
|
+
from aiogram_webhook.security.checks.check import Check
|
|
5
|
+
from aiogram_webhook.security.secret_token import SecretToken
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Security:
|
|
9
|
+
"""
|
|
10
|
+
Security class for webhook request verification.
|
|
11
|
+
|
|
12
|
+
Combines secret token and custom checks for request validation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, *checks: Check, secret_token: SecretToken | None = None) -> None:
|
|
16
|
+
self._secret_token = secret_token
|
|
17
|
+
self._checks: tuple[Check, ...] = checks
|
|
18
|
+
|
|
19
|
+
async def verify(self, bot: Bot, bound_request: BoundRequest) -> bool:
|
|
20
|
+
if self._secret_token is not None:
|
|
21
|
+
ok = await self._secret_token.verify(bot=bot, bound_request=bound_request)
|
|
22
|
+
if not ok:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
for checker in self._checks:
|
|
26
|
+
ok = await checker.verify(bot=bot, bound_request=bound_request)
|
|
27
|
+
if not ok:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
async def get_secret_token(self, *, bot: Bot) -> str | None:
|
|
33
|
+
"""
|
|
34
|
+
Get the secret token for the given bot, if configured.
|
|
35
|
+
"""
|
|
36
|
+
if self._secret_token is None:
|
|
37
|
+
return None
|
|
38
|
+
return self._secret_token.secret_token(bot=bot)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiogram-webhook
|
|
3
|
+
Version: 0.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
|
+
Project-URL: Homepage, https://github.com/m-xim/aiogram-webhook
|
|
6
|
+
Project-URL: Repository, https://github.com/m-xim/aiogram-webhook
|
|
7
|
+
Project-URL: Issues, https://github.com/m-xim/aiogram-webhook/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/m-xim/aiogram-webhook#readme
|
|
9
|
+
Author-email: m-xim <i@m-xim.ru>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: aiogram,fastapi,multibot,telegram,webhook
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: aiogram>=3.23.0
|
|
22
|
+
Requires-Dist: yarl>=1.22.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# aiogram-webhook
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/aiogram-webhook)
|
|
28
|
+
[](/LICENSE)
|
|
29
|
+
[](https://github.com/m-xim/aiogram-webhook/actions)
|
|
30
|
+
[](https://github.com/m-xim/aiogram-webhook/actions)
|
|
31
|
+
[](https://deepwiki.com/m-xim/aiogram-webhook)
|
|
32
|
+
|
|
33
|
+
**aiogram-webhook** is a Python library for seamless webhook integration with multiple web frameworks in aiogram. It enables both single and multi-bot operation via webhooks, with flexible routing and security features.
|
|
34
|
+
|
|
35
|
+
<br>
|
|
36
|
+
|
|
37
|
+
## ✨ Features
|
|
38
|
+
|
|
39
|
+
- 🧱 Modular and extensible webhook engine
|
|
40
|
+
- 🔀 Flexible routing (static and token-based)
|
|
41
|
+
- 🤖 Supports single and multi-bot setups
|
|
42
|
+
- ⚡ FastAPI adapters out of the box
|
|
43
|
+
- 🔒 Security best practices: secret tokens, IP checks
|
|
44
|
+
- 🧩 Easy to extend with custom adapters and routing
|
|
45
|
+
|
|
46
|
+
## 🚀 Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv add aiogram-webhook
|
|
50
|
+
# or
|
|
51
|
+
pip install aiogram-webhook
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## ⚡ Quick Start
|
|
55
|
+
|
|
56
|
+
### Single Bot Example (FastAPI)
|
|
57
|
+
```python
|
|
58
|
+
import uvicorn
|
|
59
|
+
from contextlib import asynccontextmanager
|
|
60
|
+
from fastapi import FastAPI
|
|
61
|
+
from aiogram import Bot, Dispatcher, Router
|
|
62
|
+
from aiogram.filters import CommandStart
|
|
63
|
+
from aiogram.types import Message
|
|
64
|
+
from aiogram_webhook import SimpleEngine, FastApiWebAdapter
|
|
65
|
+
from aiogram_webhook.routing import PathRouting
|
|
66
|
+
|
|
67
|
+
router = Router()
|
|
68
|
+
|
|
69
|
+
@router.message(CommandStart())
|
|
70
|
+
async def start(message: Message):
|
|
71
|
+
await message.answer("OK")
|
|
72
|
+
|
|
73
|
+
dispatcher = Dispatcher()
|
|
74
|
+
dispatcher.include_router(router)
|
|
75
|
+
bot = Bot("BOT_TOKEN_HERE")
|
|
76
|
+
|
|
77
|
+
engine = SimpleEngine(
|
|
78
|
+
dispatcher,
|
|
79
|
+
bot,
|
|
80
|
+
web_adapter=FastApiWebAdapter(),
|
|
81
|
+
routing=PathRouting(url="/webhook"),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@asynccontextmanager
|
|
85
|
+
async def lifespan(app: FastAPI):
|
|
86
|
+
engine.register(app)
|
|
87
|
+
await engine.set_webhook(
|
|
88
|
+
drop_pending_updates=True,
|
|
89
|
+
allowed_updates=("message", "callback_query"),
|
|
90
|
+
)
|
|
91
|
+
await engine.on_startup()
|
|
92
|
+
yield
|
|
93
|
+
await engine.on_shutdown()
|
|
94
|
+
|
|
95
|
+
app = FastAPI(lifespan=lifespan)
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
uvicorn.run("main:app", host="0.0.0.0", port=8080)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Multi-Bot Example (FastAPI)
|
|
102
|
+
Each bot is configured in Telegram with its own webhook URL: `https://example.com/webhook/<BOT_TOKEN>`
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from aiogram import Dispatcher
|
|
106
|
+
from aiogram.client.default import DefaultBotProperties
|
|
107
|
+
from aiogram_webhook import TokenEngine, FastApiWebAdapter
|
|
108
|
+
from aiogram_webhook.routing import PathRouting
|
|
109
|
+
|
|
110
|
+
dispatcher = Dispatcher()
|
|
111
|
+
engine = TokenEngine(
|
|
112
|
+
dispatcher,
|
|
113
|
+
web_adapter=FastApiWebAdapter(),
|
|
114
|
+
routing=PathRouting(url="/webhook/{bot_token}", param="bot_token"),
|
|
115
|
+
bot_settings={
|
|
116
|
+
"default": DefaultBotProperties(parse_mode="HTML"),
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Usage is the same:
|
|
122
|
+
```python
|
|
123
|
+
engine.register(app)
|
|
124
|
+
await engine.set_webhook(...)
|
|
125
|
+
await engine.on_startup()
|
|
126
|
+
await engine.on_shutdown()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 🛣️ Routing
|
|
130
|
+
|
|
131
|
+
`PathRouting` defines where Telegram sends updates:
|
|
132
|
+
|
|
133
|
+
- **Static path:**
|
|
134
|
+
```python
|
|
135
|
+
PathRouting(url="/webhook")
|
|
136
|
+
```
|
|
137
|
+
- **Token-based path:**
|
|
138
|
+
```python
|
|
139
|
+
PathRouting(url="/webhook/{bot_token}", param="bot_token")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## 🛡️ Security
|
|
143
|
+
writings...
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
aiogram_webhook/__init__.py,sha256=0rHHTrVd6q3zVPUuISvdkbRigzks9b5C9hTqRKckvH4,380
|
|
2
|
+
aiogram_webhook/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
aiogram_webhook/adapters/base.py,sha256=5TYBW3hHH5Dyo_hJ9J2xbkAqxUqpJaOI6felPqwZza0,1806
|
|
4
|
+
aiogram_webhook/adapters/fastapi.py,sha256=p7hjo8swJJU3qr7mLVbiITt3sdElJiUZGwm4syLsVk8,1415
|
|
5
|
+
aiogram_webhook/engines/__init__.py,sha256=5yqqsJi8Q2Zn_7PBNJJPebTWnGV2roOjiLoLZZiLOCE,225
|
|
6
|
+
aiogram_webhook/engines/base.py,sha256=j1QIUBnVAEQX89HnjN1yqJZiWr5IK1GCK3xZ7TnhAC0,5236
|
|
7
|
+
aiogram_webhook/engines/simple.py,sha256=HvbXemzjxOQTSVMweSX11TrTjfbuFhH_2qQaIGY0Z4Y,3326
|
|
8
|
+
aiogram_webhook/engines/token.py,sha256=YqoghLSjRx-GJvE6EaodllupKmynEygriEB-8xg00xA,3471
|
|
9
|
+
aiogram_webhook/routing/__init__.py,sha256=xJkA4CkIWmj9ynhD5CUFo_8rtfUaqcfkiH9Btyzq7Q0,219
|
|
10
|
+
aiogram_webhook/routing/base.py,sha256=n4DsCY3Ipk6Q_apri5FZIeulm5LRiLNQYG9V4OIBbNA,894
|
|
11
|
+
aiogram_webhook/routing/path.py,sha256=0TqV73B8Bf4F7Z8IszK6fYj7ArOPPNVwkL3D3G5y5p8,829
|
|
12
|
+
aiogram_webhook/routing/query.py,sha256=IedHWu-PtmQppK_oOb4NExqI6hXjI9zG0ClFn_U9nXs,1054
|
|
13
|
+
aiogram_webhook/security/__init__.py,sha256=JW-bI0yZkkD70AGlBsNNcEAsEJXfSIWPDUzWLWyvNgQ,327
|
|
14
|
+
aiogram_webhook/security/secret_token.py,sha256=HqBQbB43e0ae3Wd4ZBs6H8_0VomWdBG9H5HweQlqclY,1124
|
|
15
|
+
aiogram_webhook/security/security.py,sha256=NA9iKski7hn_uzKyUEb3mhCoPdG9cl5gL3vDq3WMnIQ,1252
|
|
16
|
+
aiogram_webhook/security/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
aiogram_webhook/security/checks/check.py,sha256=jhE4Ilk8hlXhg8x8Z3v-TXrgaVCdDN4Tj33VfKM6-gY,313
|
|
18
|
+
aiogram_webhook/security/checks/ip.py,sha256=l_i9fW_b72jIuwIkfVOF5ycHXm611R89mDk-UVu3QK4,2265
|
|
19
|
+
aiogram_webhook-0.0.1.dist-info/METADATA,sha256=xCflibZ2ZKBlhMwdqRBo9cg_X32q_iPMdbD23QH3r2w,4485
|
|
20
|
+
aiogram_webhook-0.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
21
|
+
aiogram_webhook-0.0.1.dist-info/licenses/LICENSE,sha256=18BsTl9ZVeMudRIrRyZqTd80Z3fs2qHyLJZj0wDMrZc,1062
|
|
22
|
+
aiogram_webhook-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 m-xim
|
|
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.
|