Added possibility to reply into webhook with files (#1120)

This commit is contained in:
Alex Root Junior 2023-02-12 02:00:42 +02:00 committed by GitHub
parent 84bc0e347b
commit 58868ec627
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 126 additions and 30 deletions

1
CHANGES/1120.misc.rst Normal file
View file

@ -0,0 +1 @@
Added possibility to reply into webhook with files

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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")

View file

@ -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,