diff --git a/aiogram/client/telegram.py b/aiogram/client/telegram.py
index db653249..dfededb8 100644
--- a/aiogram/client/telegram.py
+++ b/aiogram/client/telegram.py
@@ -1,7 +1,7 @@
from dataclasses import dataclass
-@dataclass
+@dataclass(frozen=True)
class TelegramAPIServer:
"""
Base config for API Endpoints
@@ -9,10 +9,11 @@ class TelegramAPIServer:
base: str
file: str
+ is_local: bool = False
def api_url(self, token: str, method: str) -> str:
"""
- Generate URL for methods
+ Generate URL for API methods
:param token: Bot token
:param method: API method name (case insensitive)
@@ -30,9 +31,15 @@ class TelegramAPIServer:
"""
return self.file.format(token=token, path=path)
+ @classmethod
+ def from_base(cls, base: str, is_local: bool = False) -> "TelegramAPIServer":
+ base = base.rstrip("/")
+ return cls(
+ base=f"{base}/bot{{token}}/{{method}}",
+ file=f"{base}/file/bot{{token}}/{{path}}",
+ is_local=is_local,
+ )
+
# Main API server
-PRODUCTION = TelegramAPIServer(
- base="https://api.telegram.org/bot{token}/{method}",
- file="https://api.telegram.org/file/bot{token}/{path}",
-)
+PRODUCTION = TelegramAPIServer.from_base("https://api.telegram.org")
diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py
index a9b8aab7..ff0bf1a7 100644
--- a/aiogram/dispatcher/dispatcher.py
+++ b/aiogram/dispatcher/dispatcher.py
@@ -247,7 +247,7 @@ class Dispatcher(Router):
raise
async def feed_webhook_update(
- self, bot: Bot, update: Union[Update, Dict[str, Any]], _timeout: int = 55, **kwargs: Any
+ self, bot: Bot, update: Union[Update, Dict[str, Any]], _timeout: float = 55, **kwargs: Any
) -> Optional[Dict[str, Any]]:
if not isinstance(update, Update): # Allow to use raw updates
update = Update(**update)
diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py
index db074975..6377223b 100644
--- a/aiogram/utils/text_decorations.py
+++ b/aiogram/utils/text_decorations.py
@@ -9,9 +9,9 @@ if TYPE_CHECKING: # pragma: no cover
from aiogram.types import MessageEntity
__all__ = (
- "TextDecoration",
"HtmlDecoration",
"MarkdownDecoration",
+ "TextDecoration",
"html_decoration",
"markdown_decoration",
)
@@ -57,14 +57,15 @@ class TextDecoration(ABC):
"""
result = "".join(
self._unparse_entities(
- text, sorted(entities, key=lambda item: item.offset) if entities else []
+ self._add_surrogates(text),
+ sorted(entities, key=lambda item: item.offset) if entities else [],
)
)
return result
def _unparse_entities(
self,
- text: str,
+ text: bytes,
entities: List[MessageEntity],
offset: Optional[int] = None,
length: Optional[int] = None,
@@ -74,21 +75,31 @@ class TextDecoration(ABC):
length = length or len(text)
for index, entity in enumerate(entities):
- if entity.offset < offset:
+ if entity.offset * 2 < offset:
continue
- if entity.offset > offset:
- yield self.quote(text[offset : entity.offset])
- start = entity.offset
- offset = entity.offset + entity.length
+ if entity.offset * 2 > offset:
+ yield self.quote(self._remove_surrogates(text[offset : entity.offset * 2]))
+ start = entity.offset * 2
+ offset = entity.offset * 2 + entity.length * 2
- sub_entities = list(filter(lambda e: e.offset < (offset or 0), entities[index + 1 :]))
+ sub_entities = list(
+ filter(lambda e: e.offset * 2 < (offset or 0), entities[index + 1 :])
+ )
yield self.apply_entity(
entity,
"".join(self._unparse_entities(text, sub_entities, offset=start, length=offset)),
)
if offset < length:
- yield self.quote(text[offset:length])
+ yield self.quote(self._remove_surrogates(text[offset:length]))
+
+ @staticmethod
+ def _add_surrogates(text: str) -> bytes:
+ return text.encode("utf-16-le")
+
+ @staticmethod
+ def _remove_surrogates(text: bytes) -> str:
+ return text.decode("utf-16-le")
@abstractmethod
def link(self, value: str, link: str) -> str: # pragma: no cover
@@ -153,11 +164,11 @@ class HtmlDecoration(TextDecoration):
return f"{value}"
def quote(self, value: str) -> str:
- return html.escape(value)
+ return html.escape(value, quote=False)
class MarkdownDecoration(TextDecoration):
- MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-|{}.!])")
+ MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])")
def link(self, value: str, link: str) -> str:
return f"[{value}]({link})"
@@ -166,7 +177,7 @@ class MarkdownDecoration(TextDecoration):
return f"*{value}*"
def italic(self, value: str) -> str:
- return f"_{value}_\r"
+ return f"_\r{value}_\r"
def code(self, value: str) -> str:
return f"`{value}`"
@@ -178,7 +189,7 @@ class MarkdownDecoration(TextDecoration):
return f"```{language}\n{value}\n```"
def underline(self, value: str) -> str:
- return f"__{value}__"
+ return f"__\r{value}__\r"
def strikethrough(self, value: str) -> str:
return f"~{value}~"
diff --git a/tests/test_api/test_client/test_api_server.py b/tests/test_api/test_client/test_api_server.py
index d7f7025d..6faf0c0f 100644
--- a/tests/test_api/test_client/test_api_server.py
+++ b/tests/test_api/test_client/test_api_server.py
@@ -1,4 +1,4 @@
-from aiogram.client.telegram import PRODUCTION
+from aiogram.client.telegram import PRODUCTION, TelegramAPIServer
class TestAPIServer:
@@ -9,3 +9,13 @@ class TestAPIServer:
def test_file_url(self):
file_url = PRODUCTION.file_url(token="42:TEST", path="path")
assert file_url == "https://api.telegram.org/file/bot42:TEST/path"
+
+ def test_from_base(self):
+ local_server = TelegramAPIServer.from_base("http://localhost:8081", is_local=True)
+
+ method_url = local_server.api_url("42:TEST", method="apiMethod")
+ file_url = local_server.file_url(token="42:TEST", path="path")
+
+ assert method_url == "http://localhost:8081/bot42:TEST/apiMethod"
+ assert file_url == "http://localhost:8081/file/bot42:TEST/path"
+ assert local_server.is_local
diff --git a/tests/test_api/test_methods/test_close.py b/tests/test_api/test_methods/test_close.py
index ce3ce43d..c497520e 100644
--- a/tests/test_api/test_methods/test_close.py
+++ b/tests/test_api/test_methods/test_close.py
@@ -1,4 +1,5 @@
import pytest
+
from aiogram.methods import Close, Request
from tests.mocked_bot import MockedBot
diff --git a/tests/test_api/test_methods/test_copy_message.py b/tests/test_api/test_methods/test_copy_message.py
index f0f1812c..d380f24a 100644
--- a/tests/test_api/test_methods/test_copy_message.py
+++ b/tests/test_api/test_methods/test_copy_message.py
@@ -1,4 +1,5 @@
import pytest
+
from aiogram.methods import CopyMessage, Request
from aiogram.types import MessageId
from tests.mocked_bot import MockedBot
@@ -10,7 +11,9 @@ class TestCopyMessage:
prepare_result = bot.add_result_for(CopyMessage, ok=True, result=MessageId(message_id=42))
response: MessageId = await CopyMessage(
- chat_id=42, from_chat_id=42, message_id=42,
+ chat_id=42,
+ from_chat_id=42,
+ message_id=42,
)
request: Request = bot.get_request()
assert request.method == "copyMessage"
@@ -22,7 +25,9 @@ class TestCopyMessage:
prepare_result = bot.add_result_for(CopyMessage, ok=True, result=MessageId(message_id=42))
response: MessageId = await bot.copy_message(
- chat_id=42, from_chat_id=42, message_id=42,
+ chat_id=42,
+ from_chat_id=42,
+ message_id=42,
)
request: Request = bot.get_request()
assert request.method == "copyMessage"
diff --git a/tests/test_api/test_methods/test_log_out.py b/tests/test_api/test_methods/test_log_out.py
index bc9149f3..e000540f 100644
--- a/tests/test_api/test_methods/test_log_out.py
+++ b/tests/test_api/test_methods/test_log_out.py
@@ -1,4 +1,5 @@
import pytest
+
from aiogram.methods import LogOut, Request
from tests.mocked_bot import MockedBot
diff --git a/tests/test_api/test_methods/test_unpin_all_chat_messages.py b/tests/test_api/test_methods/test_unpin_all_chat_messages.py
index edb2adfc..48348dfd 100644
--- a/tests/test_api/test_methods/test_unpin_all_chat_messages.py
+++ b/tests/test_api/test_methods/test_unpin_all_chat_messages.py
@@ -1,4 +1,5 @@
import pytest
+
from aiogram.methods import Request, UnpinAllChatMessages
from tests.mocked_bot import MockedBot
@@ -8,7 +9,9 @@ class TestUnpinAllChatMessages:
async def test_method(self, bot: MockedBot):
prepare_result = bot.add_result_for(UnpinAllChatMessages, ok=True, result=True)
- response: bool = await UnpinAllChatMessages(chat_id=42,)
+ response: bool = await UnpinAllChatMessages(
+ chat_id=42,
+ )
request: Request = bot.get_request()
assert request.method == "unpinAllChatMessages"
# assert request.data == {}
@@ -18,7 +21,9 @@ class TestUnpinAllChatMessages:
async def test_bot_method(self, bot: MockedBot):
prepare_result = bot.add_result_for(UnpinAllChatMessages, ok=True, result=True)
- response: bool = await bot.unpin_all_chat_messages(chat_id=42,)
+ response: bool = await bot.unpin_all_chat_messages(
+ chat_id=42,
+ )
request: Request = bot.get_request()
assert request.method == "unpinAllChatMessages"
# assert request.data == {}
diff --git a/tests/test_dispatcher/test_dispatcher.py b/tests/test_dispatcher/test_dispatcher.py
index e9cfc495..a99b1bd0 100644
--- a/tests/test_dispatcher/test_dispatcher.py
+++ b/tests/test_dispatcher/test_dispatcher.py
@@ -37,12 +37,12 @@ except ImportError:
async def simple_message_handler(message: Message):
- await asyncio.sleep(1.5)
+ await asyncio.sleep(0.2)
return message.answer("ok")
async def invalid_message_handler(message: Message):
- await asyncio.sleep(1.5)
+ await asyncio.sleep(0.2)
raise Exception(42)
@@ -578,21 +578,11 @@ class TestDispatcher:
dispatcher = Dispatcher()
dispatcher.message.register(simple_message_handler)
- response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=2)
+ response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.3)
assert isinstance(response, dict)
assert response["method"] == "sendMessage"
assert response["text"] == "ok"
- # @pytest.mark.asyncio
- # async def test_feed_webhook_update_fast_process_error(self, bot: MockedBot):
- # dispatcher = Dispatcher()
- # dispatcher.message_handler.register(invalid_message_handler)
- #
- # response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=2)
- # assert isinstance(response, dict)
- # assert response["method"] == "sendMessage"
- # assert response["text"] == "ok"
-
@pytest.mark.asyncio
async def test_feed_webhook_update_slow_process(self, bot: MockedBot, recwarn):
warnings.simplefilter("always")
@@ -604,9 +594,9 @@ class TestDispatcher:
"aiogram.dispatcher.dispatcher.Dispatcher._silent_call_request",
new_callable=CoroutineMock,
) as mocked_silent_call_request:
- response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=1)
+ response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.1)
assert response is None
- await asyncio.sleep(1)
+ await asyncio.sleep(0.2)
mocked_silent_call_request.assert_awaited()
@pytest.mark.asyncio
@@ -616,9 +606,9 @@ class TestDispatcher:
dispatcher = Dispatcher()
dispatcher.message.register(invalid_message_handler)
- response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=1)
+ response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.1)
assert response is None
- await asyncio.sleep(1)
+ await asyncio.sleep(0.1)
log_records = [rec.message for rec in caplog.records]
assert "Cause exception while process update" in log_records[0]
diff --git a/tests/test_utils/test_markdown.py b/tests/test_utils/test_markdown.py
index 792c1bb4..12e44ccf 100644
--- a/tests/test_utils/test_markdown.py
+++ b/tests/test_utils/test_markdown.py
@@ -31,13 +31,13 @@ class TestMarkdown:
[text, ("test", "test"), None, "test test"],
[bold, ("test", "test"), " ", "*test test*"],
[hbold, ("test", "test"), " ", "test test"],
- [italic, ("test", "test"), " ", "_test test_\r"],
+ [italic, ("test", "test"), " ", "_\rtest test_\r"],
[hitalic, ("test", "test"), " ", "test test"],
[code, ("test", "test"), " ", "`test test`"],
[hcode, ("test", "test"), " ", "test test"],
[pre, ("test", "test"), " ", "```test test```"],
[hpre, ("test", "test"), " ", "
test test"], - [underline, ("test", "test"), " ", "__test test__"], + [underline, ("test", "test"), " ", "__\rtest test__\r"], [hunderline, ("test", "test"), " ", "test test"], [strikethrough, ("test", "test"), " ", "~test test~"], [hstrikethrough, ("test", "test"), " ", "