refactor(telegram): msgspec schemas and parsing (#156)

This commit is contained in:
banteg
2026-01-16 19:21:26 +04:00
committed by GitHub
parent 190b2f6d6e
commit c85ab2e2a2
10 changed files with 505 additions and 412 deletions
+3 -3
View File
@@ -141,7 +141,7 @@ class FakeBot(BotClient):
"replace_message_id": replace_message_id,
}
)
return Message(message_id=1)
return Message(message_id=1, chat=Chat(id=chat_id, type="private"))
async def send_document(
self,
@@ -164,7 +164,7 @@ class FakeBot(BotClient):
"caption": caption,
}
)
return Message(message_id=2)
return Message(message_id=2, chat=Chat(id=chat_id, type="private"))
async def edit_message_text(
self,
@@ -188,7 +188,7 @@ class FakeBot(BotClient):
"wait": wait,
}
)
return Message(message_id=message_id)
return Message(message_id=message_id, chat=Chat(id=chat_id, type="private"))
async def delete_message(self, chat_id: int, message_id: int) -> bool:
self.delete_calls.append({"chat_id": chat_id, "message_id": message_id})
+31 -9
View File
@@ -9,7 +9,7 @@ from rich.text import Text
from takopi.config import ConfigError
from takopi.telegram import onboarding
from takopi.telegram.api_models import Update, User
from takopi.telegram.api_models import Chat, Message, Update, User
from takopi.telegram.client import TelegramRetryAfter
@@ -311,35 +311,57 @@ async def test_get_bot_info_gives_up(monkeypatch) -> None:
@pytest.mark.anyio
async def test_wait_for_chat_filters_updates(monkeypatch) -> None:
updates = [
[Update(update_id=1, message={"from": {"is_bot": True}, "chat": {"id": 1}})],
[
Update(
update_id=1,
message=Message(
message_id=1,
from_=User(id=1, is_bot=True),
chat=Chat(id=1, type="private"),
),
)
],
None,
[],
[Update(update_id=2, message=None)],
[
Update(
update_id=3,
message={"from": {"is_bot": True}, "chat": {"id": 2}},
message=Message(
message_id=3,
from_=User(id=2, is_bot=True),
chat=Chat(id=2, type="private"),
),
)
],
[
Update(
update_id=4,
message={"from": {"is_bot": False}, "chat": "nope"},
message=Message(
message_id=4,
from_=User(id=3, is_bot=True),
chat=Chat(id=3, type="private"),
),
)
],
[
Update(
update_id=5,
message={"from": {"is_bot": False}, "chat": {"id": "bad"}},
message=Message(
message_id=5,
from_=User(id=4, is_bot=True),
chat=Chat(id=4, type="private"),
),
)
],
[
Update(
update_id=6,
message={
"from": {"is_bot": False},
"chat": {"id": 7, "username": "bob", "type": "private"},
},
message=Message(
message_id=6,
from_=User(id=5, is_bot=False),
chat=Chat(id=7, username="bob", type="private"),
),
)
],
]
+2 -2
View File
@@ -14,7 +14,7 @@ from takopi.telegram.commands.topics import _handle_topic_command
import takopi.telegram.loop as telegram_loop
import takopi.telegram.topics as telegram_topics
from takopi.directives import parse_directives
from takopi.telegram.api_models import File, ForumTopic, Message, Update, User
from takopi.telegram.api_models import Chat, File, ForumTopic, Message, Update, User
from takopi.settings import TelegramFilesSettings, TelegramTopicsSettings
from takopi.telegram.bridge import (
TelegramBridgeConfig,
@@ -462,7 +462,7 @@ async def test_telegram_transport_edit_wait_false_returns_ref() -> None:
)
if not wait:
return None
return Message(message_id=message_id)
return Message(message_id=message_id, chat=Chat(id=chat_id, type="private"))
async def delete_message(
self,
+3 -3
View File
@@ -71,9 +71,9 @@ async def test_client_methods_build_params_and_decode() -> None:
payloads = {
"getUpdates": [{"update_id": 1}],
"getFile": {"file_path": "path"},
"sendMessage": {"message_id": 1},
"sendDocument": {"message_id": 2},
"editMessageText": {"message_id": 3},
"sendMessage": {"message_id": 1, "chat": {"id": 1, "type": "private"}},
"sendDocument": {"message_id": 2, "chat": {"id": 1, "type": "private"}},
"editMessageText": {"message_id": 3, "chat": {"id": 1, "type": "private"}},
"deleteMessage": True,
"setMyCommands": True,
"getMe": {"id": 7},
+156 -149
View File
@@ -3,23 +3,37 @@ from takopi.telegram import (
TelegramIncomingMessage,
parse_incoming_update,
)
from takopi.telegram.api_models import (
CallbackQuery,
CallbackQueryMessage,
Chat,
Document,
Message,
MessageReply,
PhotoSize,
Sticker,
Update,
User,
Video,
Voice,
)
def test_parse_incoming_update_maps_fields() -> None:
update = {
"update_id": 1,
"message": {
"message_id": 10,
"text": "hello",
"chat": {"id": 123, "type": "supergroup", "is_forum": True},
"from": {"id": 99},
"reply_to_message": {
"message_id": 5,
"text": "prev",
"from": {"id": 77, "is_bot": True, "username": "ReplyBot"},
},
},
}
update = Update(
update_id=1,
message=Message(
message_id=10,
text="hello",
chat=Chat(id=123, type="supergroup", is_forum=True),
from_=User(id=99),
reply_to_message=MessageReply(
message_id=5,
text="prev",
from_=User(id=77, is_bot=True, username="ReplyBot"),
),
),
)
msg = parse_incoming_update(update, chat_id=123)
assert msg is not None
@@ -39,50 +53,49 @@ def test_parse_incoming_update_maps_fields() -> None:
assert msg.is_forum is True
assert msg.voice is None
assert msg.document is None
assert msg.raw == update["message"]
assert msg.raw
assert msg.raw["message_id"] == 10
def test_parse_incoming_update_filters_non_matching_chat() -> None:
update = {
"update_id": 1,
"message": {
"message_id": 10,
"text": "hello",
"chat": {"id": 123},
},
}
update = Update(
update_id=1,
message=Message(
message_id=10,
text="hello",
chat=Chat(id=123, type="private"),
),
)
assert parse_incoming_update(update, chat_id=999) is None
def test_parse_incoming_update_filters_non_text_and_non_voice() -> None:
update = {
"update_id": 1,
"message": {
"message_id": 10,
"chat": {"id": 123},
"location": {"latitude": 1.0, "longitude": 2.0},
},
}
update = Update(
update_id=1,
message=Message(
message_id=10,
chat=Chat(id=123, type="private"),
),
)
assert parse_incoming_update(update, chat_id=123) is None
def test_parse_incoming_update_voice_message() -> None:
update = {
"update_id": 1,
"message": {
"message_id": 10,
"chat": {"id": 123},
"voice": {
"file_id": "voice-id",
"file_unique_id": "uniq",
"duration": 3,
"mime_type": "audio/ogg",
"file_size": 1234,
},
},
}
update = Update(
update_id=1,
message=Message(
message_id=10,
chat=Chat(id=123, type="private"),
voice=Voice(
file_id="voice-id",
duration=3,
mime_type="audio/ogg",
file_size=1234,
),
),
)
msg = parse_incoming_update(update, chat_id=123)
assert msg is not None
@@ -96,21 +109,20 @@ def test_parse_incoming_update_voice_message() -> None:
def test_parse_incoming_update_document_message() -> None:
update = {
"update_id": 1,
"message": {
"message_id": 10,
"caption": "/file put incoming/doc.txt",
"chat": {"id": 123},
"document": {
"file_id": "doc-id",
"file_unique_id": "uniq",
"file_name": "doc.txt",
"mime_type": "text/plain",
"file_size": 4321,
},
},
}
update = Update(
update_id=1,
message=Message(
message_id=10,
caption="/file put incoming/doc.txt",
chat=Chat(id=123, type="private"),
document=Document(
file_id="doc-id",
file_name="doc.txt",
mime_type="text/plain",
file_size=4321,
),
),
)
msg = parse_incoming_update(update, chat_id=123)
assert msg is not None
@@ -124,30 +136,28 @@ def test_parse_incoming_update_document_message() -> None:
def test_parse_incoming_update_photo_message() -> None:
update = {
"update_id": 1,
"message": {
"message_id": 10,
"caption": "/file put incoming/photo.jpg",
"chat": {"id": 123},
"photo": [
{
"file_id": "small",
"file_unique_id": "uniq-small",
"file_size": 100,
"width": 90,
"height": 90,
},
{
"file_id": "large",
"file_unique_id": "uniq-large",
"file_size": 1000,
"width": 800,
"height": 600,
},
update = Update(
update_id=1,
message=Message(
message_id=10,
caption="/file put incoming/photo.jpg",
chat=Chat(id=123, type="private"),
photo=[
PhotoSize(
file_id="small",
file_size=100,
width=90,
height=90,
),
PhotoSize(
file_id="large",
file_size=1000,
width=800,
height=600,
),
],
},
}
),
)
msg = parse_incoming_update(update, chat_id=123)
assert msg is not None
@@ -160,23 +170,22 @@ def test_parse_incoming_update_photo_message() -> None:
def test_parse_incoming_update_media_group_id() -> None:
update = {
"update_id": 1,
"message": {
"message_id": 10,
"chat": {"id": 123},
"media_group_id": "group-1",
"photo": [
{
"file_id": "large",
"file_unique_id": "uniq-large",
"file_size": 1000,
"width": 800,
"height": 600,
}
update = Update(
update_id=1,
message=Message(
message_id=10,
chat=Chat(id=123, type="private"),
media_group_id="group-1",
photo=[
PhotoSize(
file_id="large",
file_size=1000,
width=800,
height=600,
)
],
},
}
),
)
msg = parse_incoming_update(update, chat_id=123)
assert msg is not None
@@ -185,21 +194,20 @@ def test_parse_incoming_update_media_group_id() -> None:
def test_parse_incoming_update_video_message() -> None:
update = {
"update_id": 1,
"message": {
"message_id": 10,
"caption": "/file put incoming/video.mp4",
"chat": {"id": 123},
"video": {
"file_id": "video-id",
"file_unique_id": "uniq",
"file_name": "video.mp4",
"mime_type": "video/mp4",
"file_size": 4242,
},
},
}
update = Update(
update_id=1,
message=Message(
message_id=10,
caption="/file put incoming/video.mp4",
chat=Chat(id=123, type="private"),
video=Video(
file_id="video-id",
file_name="video.mp4",
mime_type="video/mp4",
file_size=4242,
),
),
)
msg = parse_incoming_update(update, chat_id=123)
assert msg is not None
@@ -213,19 +221,18 @@ def test_parse_incoming_update_video_message() -> None:
def test_parse_incoming_update_sticker_message() -> None:
update = {
"update_id": 1,
"message": {
"message_id": 10,
"caption": "/file put incoming/sticker.webp",
"chat": {"id": 123},
"sticker": {
"file_id": "sticker-id",
"file_unique_id": "uniq",
"file_size": 2468,
},
},
}
update = Update(
update_id=1,
message=Message(
message_id=10,
caption="/file put incoming/sticker.webp",
chat=Chat(id=123, type="private"),
sticker=Sticker(
file_id="sticker-id",
file_size=2468,
),
),
)
msg = parse_incoming_update(update, chat_id=123)
assert msg is not None
@@ -239,18 +246,18 @@ def test_parse_incoming_update_sticker_message() -> None:
def test_parse_incoming_update_callback_query() -> None:
update = {
"update_id": 1,
"callback_query": {
"id": "cbq-1",
"data": "takopi:cancel",
"from": {"id": 321},
"message": {
"message_id": 55,
"chat": {"id": 123},
},
},
}
update = Update(
update_id=1,
callback_query=CallbackQuery(
id="cbq-1",
data="takopi:cancel",
from_=User(id=321),
message=CallbackQueryMessage(
message_id=55,
chat=Chat(id=123, type="private"),
),
),
)
msg = parse_incoming_update(update, chat_id=123)
assert isinstance(msg, TelegramCallbackQuery)
@@ -263,16 +270,16 @@ def test_parse_incoming_update_callback_query() -> None:
def test_parse_incoming_update_topic_fields() -> None:
update = {
"update_id": 1,
"message": {
"message_id": 10,
"text": "hello",
"message_thread_id": 77,
"is_topic_message": True,
"chat": {"id": -100, "type": "supergroup", "is_forum": True},
},
}
update = Update(
update_id=1,
message=Message(
message_id=10,
text="hello",
message_thread_id=77,
is_topic_message=True,
chat=Chat(id=-100, type="supergroup", is_forum=True),
),
)
msg = parse_incoming_update(update, chat_id=-100)
assert isinstance(msg, TelegramIncomingMessage)
+4 -4
View File
@@ -3,7 +3,7 @@ from typing import Any
import anyio
import pytest
from takopi.telegram.api_models import File, Message, Update, User
from takopi.telegram.api_models import Chat, File, Message, Update, User
from takopi.telegram.client import BotClient, TelegramClient, TelegramRetryAfter
@@ -39,7 +39,7 @@ class FakeBot(BotClient):
_ = reply_markup
_ = replace_message_id
self.calls.append("send_message")
return Message(message_id=1)
return Message(message_id=1, chat=Chat(id=chat_id, type="private"))
async def send_document(
self,
@@ -61,7 +61,7 @@ class FakeBot(BotClient):
caption,
)
self.calls.append("send_document")
return Message(message_id=1)
return Message(message_id=1, chat=Chat(id=chat_id, type="private"))
async def edit_message_text(
self,
@@ -86,7 +86,7 @@ class FakeBot(BotClient):
self._edit_attempts += 1
raise TelegramRetryAfter(self.retry_after)
self._edit_attempts += 1
return Message(message_id=message_id)
return Message(message_id=message_id, chat=Chat(id=chat_id, type="private"))
async def delete_message(
self,