mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Added possibility to reply into webhook with files (#1120)
This commit is contained in:
parent
84bc0e347b
commit
58868ec627
6 changed files with 126 additions and 30 deletions
1
CHANGES/1120.misc.rst
Normal file
1
CHANGES/1120.misc.rst
Normal file
|
|
@ -0,0 +1 @@
|
|||
Added possibility to reply into webhook with files
|
||||
|
|
@ -13,7 +13,7 @@ from ..fsm.middleware import FSMContextMiddleware
|
|||
from ..fsm.storage.base import BaseEventIsolation, BaseStorage
|
||||
from ..fsm.storage.memory import DisabledEventIsolation, MemoryStorage
|
||||
from ..fsm.strategy import FSMStrategy
|
||||
from ..methods import GetUpdates, TelegramMethod
|
||||
from ..methods import GetUpdates, Request, TelegramMethod
|
||||
from ..types import Update, User
|
||||
from ..types.update import UpdateTypeLookupError
|
||||
from ..utils.backoff import Backoff, BackoffConfig
|
||||
|
|
@ -351,7 +351,7 @@ class Dispatcher(Router):
|
|||
|
||||
async def feed_webhook_update(
|
||||
self, bot: Bot, update: Union[Update, Dict[str, Any]], _timeout: float = 55, **kwargs: Any
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
) -> Optional[Request]:
|
||||
if not isinstance(update, Update): # Allow to use raw updates
|
||||
update = Update(**update)
|
||||
|
||||
|
|
@ -397,8 +397,7 @@ class Dispatcher(Router):
|
|||
# TODO: handle exceptions
|
||||
response: Any = process_updates.result()
|
||||
if isinstance(response, TelegramMethod):
|
||||
request = response.build_request(bot=bot)
|
||||
return request.render_webhook_request()
|
||||
return response.build_request(bot=bot)
|
||||
|
||||
else:
|
||||
process_updates.remove_done_callback(release_waiter)
|
||||
|
|
|
|||
|
|
@ -33,12 +33,6 @@ class Request(BaseModel):
|
|||
class Config(BaseConfig):
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def render_webhook_request(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"method": self.method,
|
||||
**{key: value for key, value in self.data.items() if value is not None},
|
||||
}
|
||||
|
||||
|
||||
class Response(GenericModel, Generic[TelegramType]):
|
||||
ok: bool
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import asyncio
|
||||
import secrets
|
||||
from abc import ABC, abstractmethod
|
||||
from asyncio import Transport
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional, Tuple, cast
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp import MultipartWriter, web
|
||||
from aiohttp.abc import Application
|
||||
from aiohttp.typedefs import Handler
|
||||
from aiohttp.web_middlewares import middleware
|
||||
|
||||
from aiogram import Bot, Dispatcher, loggers
|
||||
from aiogram.methods import TelegramMethod
|
||||
from aiogram.methods import Request, TelegramMethod
|
||||
from aiogram.types import UNSET
|
||||
from aiogram.webhook.security import IPFilter
|
||||
|
||||
|
||||
|
|
@ -84,7 +86,10 @@ class BaseRequestHandler(ABC):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, dispatcher: Dispatcher, handle_in_background: bool = True, **data: Any
|
||||
self,
|
||||
dispatcher: Dispatcher,
|
||||
handle_in_background: bool = False,
|
||||
**data: Any,
|
||||
) -> None:
|
||||
"""
|
||||
:param dispatcher: instance of :class:`aiogram.dispatcher.dispatcher.Dispatcher`
|
||||
|
|
@ -138,15 +143,39 @@ class BaseRequestHandler(ABC):
|
|||
)
|
||||
return web.json_response({}, dumps=bot.session.json_dumps)
|
||||
|
||||
def _build_response_writer(self, bot: Bot, result: Optional[Request]) -> MultipartWriter:
|
||||
writer = MultipartWriter(
|
||||
"form-data",
|
||||
boundary=f"webhookBoundary{secrets.token_urlsafe(16)}",
|
||||
)
|
||||
if not result:
|
||||
return writer
|
||||
|
||||
payload = writer.append(result.method)
|
||||
payload.set_content_disposition("form-data", name="method")
|
||||
|
||||
for key, value in result.data.items():
|
||||
if value is None or value is UNSET:
|
||||
continue
|
||||
payload = writer.append(bot.session.prepare_value(value))
|
||||
payload.set_content_disposition("form-data", name=key)
|
||||
|
||||
if not result.files:
|
||||
return writer
|
||||
|
||||
for key, value in result.files.items():
|
||||
payload = writer.append(value)
|
||||
payload.set_content_disposition("form-data", name=key, filename=value.filename)
|
||||
|
||||
return writer
|
||||
|
||||
async def _handle_request(self, bot: Bot, request: web.Request) -> web.Response:
|
||||
result = await self.dispatcher.feed_webhook_update(
|
||||
bot,
|
||||
await request.json(loads=bot.session.json_loads),
|
||||
**self.data,
|
||||
)
|
||||
if result:
|
||||
return web.json_response(result, dumps=bot.session.json_dumps)
|
||||
return web.json_response({}, dumps=bot.session.json_dumps)
|
||||
return web.Response(body=self._build_response_writer(bot=bot, result=result))
|
||||
|
||||
async def handle(self, request: web.Request) -> web.Response:
|
||||
bot = await self.resolve_bot(request)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from aiogram import Bot
|
|||
from aiogram.dispatcher.dispatcher import Dispatcher
|
||||
from aiogram.dispatcher.event.bases import UNHANDLED, SkipHandler
|
||||
from aiogram.dispatcher.router import Router
|
||||
from aiogram.methods import GetMe, GetUpdates, SendMessage
|
||||
from aiogram.methods import GetMe, GetUpdates, Request, SendMessage
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
Chat,
|
||||
|
|
@ -703,9 +703,9 @@ class TestDispatcher:
|
|||
dispatcher.message.register(simple_message_handler)
|
||||
|
||||
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.3)
|
||||
assert isinstance(response, dict)
|
||||
assert response["method"] == "sendMessage"
|
||||
assert response["text"] == "ok"
|
||||
assert isinstance(response, Request)
|
||||
assert response.method == "sendMessage"
|
||||
assert response.data["text"] == "ok"
|
||||
|
||||
async def test_feed_webhook_update_slow_process(self, bot: MockedBot, recwarn):
|
||||
warnings.simplefilter("always")
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ from typing import Any, Dict
|
|||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from aiohttp import web
|
||||
from aiohttp import MultipartReader, web
|
||||
from aiohttp.test_utils import TestClient
|
||||
from aiohttp.web_app import Application
|
||||
|
||||
from aiogram import Dispatcher, F
|
||||
from aiogram.methods import GetMe, Request
|
||||
from aiogram.types import Message, User
|
||||
from aiogram.types import BufferedInputFile, Message, User
|
||||
from aiogram.webhook.aiohttp_server import (
|
||||
SimpleRequestHandler,
|
||||
TokenBasedRequestHandler,
|
||||
|
|
@ -73,16 +73,16 @@ class TestSimpleRequestHandler:
|
|||
},
|
||||
)
|
||||
|
||||
async def test(self, bot: MockedBot, aiohttp_client):
|
||||
async def test_reply_into_webhook_file(self, bot: MockedBot, aiohttp_client):
|
||||
app = Application()
|
||||
dp = Dispatcher()
|
||||
|
||||
handler_event = Event()
|
||||
|
||||
@dp.message(F.text == "test")
|
||||
def handle_message(msg: Message):
|
||||
handler_event.set()
|
||||
return msg.answer("PASS")
|
||||
return msg.answer_document(
|
||||
caption="PASS",
|
||||
document=BufferedInputFile(b"test", filename="test.txt"),
|
||||
)
|
||||
|
||||
handler = SimpleRequestHandler(
|
||||
dispatcher=dp,
|
||||
|
|
@ -94,15 +94,88 @@ class TestSimpleRequestHandler:
|
|||
|
||||
resp = await self.make_reqest(client=client)
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
assert resp.content_type == "multipart/form-data"
|
||||
result = {}
|
||||
reader = MultipartReader.from_response(resp)
|
||||
while part := await reader.next():
|
||||
value = await part.read()
|
||||
result[part.name] = value.decode()
|
||||
assert result["method"] == "sendDocument"
|
||||
assert result["caption"] == "PASS"
|
||||
assert result["document"] == "test"
|
||||
|
||||
async def test_reply_into_webhook_text(self, bot: MockedBot, aiohttp_client):
|
||||
app = Application()
|
||||
dp = Dispatcher()
|
||||
|
||||
@dp.message(F.text == "test")
|
||||
def handle_message(msg: Message):
|
||||
return msg.answer(text="PASS")
|
||||
|
||||
handler = SimpleRequestHandler(
|
||||
dispatcher=dp,
|
||||
bot=bot,
|
||||
handle_in_background=False,
|
||||
)
|
||||
handler.register(app, path="/webhook")
|
||||
client: TestClient = await aiohttp_client(app)
|
||||
|
||||
resp = await self.make_reqest(client=client)
|
||||
assert resp.status == 200
|
||||
assert resp.content_type == "multipart/form-data"
|
||||
result = {}
|
||||
reader = MultipartReader.from_response(resp)
|
||||
while part := await reader.next():
|
||||
value = await part.read()
|
||||
result[part.name] = value.decode()
|
||||
assert result["method"] == "sendMessage"
|
||||
assert result["text"] == "PASS"
|
||||
|
||||
async def test_reply_into_webhook_unhandled(self, bot: MockedBot, aiohttp_client):
|
||||
app = Application()
|
||||
dp = Dispatcher()
|
||||
|
||||
@dp.message(F.text == "test")
|
||||
def handle_message(msg: Message):
|
||||
return msg.answer(text="PASS")
|
||||
|
||||
handler = SimpleRequestHandler(
|
||||
dispatcher=dp,
|
||||
bot=bot,
|
||||
handle_in_background=False,
|
||||
)
|
||||
handler.register(app, path="/webhook")
|
||||
client: TestClient = await aiohttp_client(app)
|
||||
|
||||
resp = await self.make_reqest(client=client, text="spam")
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
assert resp.content_type == "multipart/form-data"
|
||||
result = {}
|
||||
reader = MultipartReader.from_response(resp)
|
||||
while part := await reader.next():
|
||||
value = await part.read()
|
||||
result[part.name] = value.decode()
|
||||
assert not result
|
||||
|
||||
handler.handle_in_background = True
|
||||
async def test_reply_into_webhook_background(self, bot: MockedBot, aiohttp_client):
|
||||
app = Application()
|
||||
dp = Dispatcher()
|
||||
|
||||
handler_event = Event()
|
||||
|
||||
@dp.message(F.text == "test")
|
||||
def handle_message(msg: Message):
|
||||
handler_event.set()
|
||||
return msg.answer(text="PASS")
|
||||
|
||||
handler = SimpleRequestHandler(
|
||||
dispatcher=dp,
|
||||
bot=bot,
|
||||
handle_in_background=True,
|
||||
)
|
||||
handler.register(app, path="/webhook")
|
||||
client: TestClient = await aiohttp_client(app)
|
||||
|
||||
with patch(
|
||||
"aiogram.dispatcher.dispatcher.Dispatcher.silent_call_request",
|
||||
new_callable=AsyncMock,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue