refactor(telegram): msgspec schemas and parsing (#156)
This commit is contained in:
@@ -1,53 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import msgspec
|
||||
from .api_schemas import (
|
||||
CallbackQuery,
|
||||
CallbackQueryMessage,
|
||||
Chat,
|
||||
ChatMember,
|
||||
Document,
|
||||
File,
|
||||
ForumTopic,
|
||||
Message,
|
||||
MessageReply,
|
||||
PhotoSize,
|
||||
Sticker,
|
||||
Update,
|
||||
User,
|
||||
Video,
|
||||
Voice,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CallbackQuery",
|
||||
"CallbackQueryMessage",
|
||||
"Chat",
|
||||
"ChatMember",
|
||||
"Document",
|
||||
"File",
|
||||
"ForumTopic",
|
||||
"Message",
|
||||
"MessageReply",
|
||||
"PhotoSize",
|
||||
"Sticker",
|
||||
"Update",
|
||||
"User",
|
||||
"Video",
|
||||
"Voice",
|
||||
]
|
||||
|
||||
|
||||
class User(msgspec.Struct, forbid_unknown_fields=False):
|
||||
id: int
|
||||
username: str | None = None
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
|
||||
|
||||
class Chat(msgspec.Struct, forbid_unknown_fields=False):
|
||||
id: int
|
||||
type: str
|
||||
is_forum: bool | None = None
|
||||
|
||||
|
||||
class ChatMember(msgspec.Struct, forbid_unknown_fields=False):
|
||||
status: str
|
||||
can_manage_topics: bool | None = None
|
||||
|
||||
|
||||
class Message(msgspec.Struct, forbid_unknown_fields=False):
|
||||
message_id: int
|
||||
message_thread_id: int | None = None
|
||||
text: str | None = None
|
||||
|
||||
|
||||
class File(msgspec.Struct, forbid_unknown_fields=False):
|
||||
file_path: str
|
||||
|
||||
|
||||
class ForumTopic(msgspec.Struct, forbid_unknown_fields=False):
|
||||
message_thread_id: int
|
||||
|
||||
|
||||
class Update(msgspec.Struct, forbid_unknown_fields=False):
|
||||
update_id: int
|
||||
message: dict[str, Any] | None = None
|
||||
callback_query: dict[str, Any] | None = None
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Msgspec models for Telegram Bot API payloads (subset used by takopi).
|
||||
|
||||
Derived from telegram-api.html in the repository.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import msgspec
|
||||
|
||||
__all__ = [
|
||||
"CallbackQuery",
|
||||
"CallbackQueryMessage",
|
||||
"Chat",
|
||||
"ChatMember",
|
||||
"Document",
|
||||
"File",
|
||||
"ForumTopic",
|
||||
"Message",
|
||||
"MessageReply",
|
||||
"PhotoSize",
|
||||
"Sticker",
|
||||
"Update",
|
||||
"User",
|
||||
"Video",
|
||||
"Voice",
|
||||
"decode_update",
|
||||
"decode_updates",
|
||||
]
|
||||
|
||||
|
||||
class User(msgspec.Struct, forbid_unknown_fields=False):
|
||||
id: int
|
||||
is_bot: bool | None = None
|
||||
username: str | None = None
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
|
||||
|
||||
class Chat(msgspec.Struct, forbid_unknown_fields=False):
|
||||
id: int
|
||||
type: str
|
||||
title: str | None = None
|
||||
username: str | None = None
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
is_forum: bool | None = None
|
||||
|
||||
|
||||
class PhotoSize(msgspec.Struct, forbid_unknown_fields=False):
|
||||
file_id: str
|
||||
width: int
|
||||
height: int
|
||||
file_size: int | None = None
|
||||
|
||||
|
||||
class Document(msgspec.Struct, forbid_unknown_fields=False):
|
||||
file_id: str
|
||||
file_name: str | None = None
|
||||
mime_type: str | None = None
|
||||
file_size: int | None = None
|
||||
|
||||
|
||||
class Video(msgspec.Struct, forbid_unknown_fields=False):
|
||||
file_id: str
|
||||
file_name: str | None = None
|
||||
mime_type: str | None = None
|
||||
file_size: int | None = None
|
||||
|
||||
|
||||
class Voice(msgspec.Struct, forbid_unknown_fields=False):
|
||||
file_id: str
|
||||
duration: int | None = None
|
||||
mime_type: str | None = None
|
||||
file_size: int | None = None
|
||||
|
||||
|
||||
class Sticker(msgspec.Struct, forbid_unknown_fields=False):
|
||||
file_id: str
|
||||
file_size: int | None = None
|
||||
|
||||
|
||||
class MessageReply(msgspec.Struct, forbid_unknown_fields=False):
|
||||
message_id: int
|
||||
text: str | None = None
|
||||
from_: User | None = msgspec.field(default=None, name="from")
|
||||
|
||||
|
||||
class Message(msgspec.Struct, forbid_unknown_fields=False):
|
||||
message_id: int
|
||||
chat: Chat
|
||||
message_thread_id: int | None = None
|
||||
from_: User | None = msgspec.field(default=None, name="from")
|
||||
text: str | None = None
|
||||
caption: str | None = None
|
||||
reply_to_message: MessageReply | None = None
|
||||
forward_from: User | None = None
|
||||
forward_from_chat: Chat | None = None
|
||||
forward_from_message_id: int | None = None
|
||||
forward_sender_name: str | None = None
|
||||
forward_signature: str | None = None
|
||||
forward_date: int | None = None
|
||||
media_group_id: str | None = None
|
||||
is_automatic_forward: bool | None = None
|
||||
is_topic_message: bool | None = None
|
||||
voice: Voice | None = None
|
||||
document: Document | None = None
|
||||
video: Video | None = None
|
||||
photo: list[PhotoSize] | None = None
|
||||
sticker: Sticker | None = None
|
||||
|
||||
|
||||
class CallbackQueryMessage(msgspec.Struct, forbid_unknown_fields=False):
|
||||
message_id: int
|
||||
chat: Chat
|
||||
|
||||
|
||||
class CallbackQuery(msgspec.Struct, forbid_unknown_fields=False):
|
||||
id: str
|
||||
from_: User = msgspec.field(name="from")
|
||||
message: CallbackQueryMessage | None = None
|
||||
data: str | None = None
|
||||
|
||||
|
||||
class Update(msgspec.Struct, forbid_unknown_fields=False):
|
||||
update_id: int
|
||||
message: Message | None = None
|
||||
callback_query: CallbackQuery | None = None
|
||||
|
||||
|
||||
class File(msgspec.Struct, forbid_unknown_fields=False):
|
||||
file_path: str
|
||||
|
||||
|
||||
class ChatMember(msgspec.Struct, forbid_unknown_fields=False):
|
||||
status: str
|
||||
can_manage_topics: bool | None = None
|
||||
|
||||
|
||||
class ForumTopic(msgspec.Struct, forbid_unknown_fields=False):
|
||||
message_thread_id: int
|
||||
|
||||
|
||||
_UPDATE_DECODER = msgspec.json.Decoder(Update)
|
||||
_UPDATES_DECODER = msgspec.json.Decoder(list[Update])
|
||||
|
||||
|
||||
def decode_update(payload: str | bytes) -> Update:
|
||||
return _UPDATE_DECODER.decode(payload)
|
||||
|
||||
|
||||
def decode_updates(payload: str | bytes) -> list[Update]:
|
||||
return _UPDATES_DECODER.decode(payload)
|
||||
@@ -264,35 +264,25 @@ async def wait_for_chat(token: str) -> ChatInfo:
|
||||
continue
|
||||
if not updates:
|
||||
continue
|
||||
offset = updates[-1].update_id + 1
|
||||
update = updates[-1]
|
||||
offset = update.update_id + 1
|
||||
msg = update.message
|
||||
if not isinstance(msg, dict):
|
||||
if msg is None:
|
||||
continue
|
||||
sender = msg.get("from")
|
||||
if isinstance(sender, dict) and sender.get("is_bot") is True:
|
||||
sender = msg.from_
|
||||
if sender is not None and sender.is_bot is True:
|
||||
continue
|
||||
chat = msg.get("chat")
|
||||
if not isinstance(chat, dict):
|
||||
continue
|
||||
chat_id = chat.get("id")
|
||||
if not isinstance(chat_id, int):
|
||||
chat = msg.chat
|
||||
if chat is None:
|
||||
continue
|
||||
chat_id = chat.id
|
||||
return ChatInfo(
|
||||
chat_id=chat_id,
|
||||
username=chat.get("username")
|
||||
if isinstance(chat.get("username"), str)
|
||||
else None,
|
||||
title=chat.get("title") if isinstance(chat.get("title"), str) else None,
|
||||
first_name=chat.get("first_name")
|
||||
if isinstance(chat.get("first_name"), str)
|
||||
else None,
|
||||
last_name=chat.get("last_name")
|
||||
if isinstance(chat.get("last_name"), str)
|
||||
else None,
|
||||
chat_type=chat.get("type")
|
||||
if isinstance(chat.get("type"), str)
|
||||
else None,
|
||||
username=chat.username,
|
||||
title=chat.title,
|
||||
first_name=chat.first_name,
|
||||
last_name=chat.last_name,
|
||||
chat_type=chat.type,
|
||||
)
|
||||
finally:
|
||||
await bot.close()
|
||||
|
||||
+117
-179
@@ -1,12 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from collections.abc import AsyncIterator, Callable, Iterable
|
||||
|
||||
import anyio
|
||||
import msgspec
|
||||
|
||||
from ..logging import get_logger
|
||||
from .api_models import Update
|
||||
from .api_schemas import (
|
||||
CallbackQuery,
|
||||
Document,
|
||||
Message,
|
||||
PhotoSize,
|
||||
Sticker,
|
||||
Update,
|
||||
Video,
|
||||
)
|
||||
from .client_api import BotClient
|
||||
from .types import (
|
||||
TelegramCallbackQuery,
|
||||
@@ -20,23 +28,20 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
def parse_incoming_update(
|
||||
update: Update | dict[str, Any],
|
||||
update: Update,
|
||||
*,
|
||||
chat_id: int | None = None,
|
||||
chat_ids: set[int] | None = None,
|
||||
) -> TelegramIncomingUpdate | None:
|
||||
if isinstance(update, Update):
|
||||
msg = update.message
|
||||
callback_query = update.callback_query
|
||||
else:
|
||||
msg = update.get("message")
|
||||
callback_query = update.get("callback_query")
|
||||
|
||||
if isinstance(msg, dict):
|
||||
return _parse_incoming_message(msg, chat_id=chat_id, chat_ids=chat_ids)
|
||||
if isinstance(callback_query, dict):
|
||||
if update.message is not None:
|
||||
return _parse_incoming_message(
|
||||
update.message,
|
||||
chat_id=chat_id,
|
||||
chat_ids=chat_ids,
|
||||
)
|
||||
if update.callback_query is not None:
|
||||
return _parse_callback_query(
|
||||
callback_query,
|
||||
update.callback_query,
|
||||
chat_id=chat_id,
|
||||
chat_ids=chat_ids,
|
||||
)
|
||||
@@ -44,167 +49,71 @@ def parse_incoming_update(
|
||||
|
||||
|
||||
def _parse_incoming_message(
|
||||
msg: dict[str, Any],
|
||||
msg: Message,
|
||||
*,
|
||||
chat_id: int | None = None,
|
||||
chat_ids: set[int] | None = None,
|
||||
) -> TelegramIncomingMessage | None:
|
||||
def _parse_document_payload(payload: dict[str, Any]) -> TelegramDocument | None:
|
||||
file_id = payload.get("file_id")
|
||||
if not isinstance(file_id, str) or not file_id:
|
||||
return None
|
||||
return TelegramDocument(
|
||||
file_id=file_id,
|
||||
file_name=payload.get("file_name")
|
||||
if isinstance(payload.get("file_name"), str)
|
||||
else None,
|
||||
mime_type=payload.get("mime_type")
|
||||
if isinstance(payload.get("mime_type"), str)
|
||||
else None,
|
||||
file_size=payload.get("file_size")
|
||||
if isinstance(payload.get("file_size"), int)
|
||||
and not isinstance(payload.get("file_size"), bool)
|
||||
else None,
|
||||
raw=payload,
|
||||
)
|
||||
|
||||
raw_text = msg.get("text")
|
||||
text = raw_text if isinstance(raw_text, str) else None
|
||||
caption = msg.get("caption")
|
||||
if text is None and isinstance(caption, str):
|
||||
text = caption
|
||||
raw_text = msg.text
|
||||
caption = msg.caption
|
||||
text = raw_text if raw_text is not None else caption
|
||||
if text is None:
|
||||
text = ""
|
||||
file_command = False
|
||||
if isinstance(text, str):
|
||||
stripped = text.lstrip()
|
||||
if stripped.startswith("/"):
|
||||
token = stripped.split(maxsplit=1)[0]
|
||||
file_command = token.startswith("/file")
|
||||
stripped = text.lstrip()
|
||||
if stripped.startswith("/"):
|
||||
token = stripped.split(maxsplit=1)[0]
|
||||
file_command = token.startswith("/file")
|
||||
voice_payload: TelegramVoice | None = None
|
||||
voice = msg.get("voice")
|
||||
if isinstance(voice, dict):
|
||||
file_id = voice.get("file_id")
|
||||
if not isinstance(file_id, str) or not file_id:
|
||||
file_id = None
|
||||
if file_id is not None:
|
||||
voice_payload = TelegramVoice(
|
||||
file_id=file_id,
|
||||
mime_type=voice.get("mime_type")
|
||||
if isinstance(voice.get("mime_type"), str)
|
||||
else None,
|
||||
file_size=voice.get("file_size")
|
||||
if isinstance(voice.get("file_size"), int)
|
||||
and not isinstance(voice.get("file_size"), bool)
|
||||
else None,
|
||||
duration=voice.get("duration")
|
||||
if isinstance(voice.get("duration"), int)
|
||||
and not isinstance(voice.get("duration"), bool)
|
||||
else None,
|
||||
raw=voice,
|
||||
)
|
||||
if not isinstance(raw_text, str) and not isinstance(caption, str):
|
||||
text = ""
|
||||
if msg.voice is not None:
|
||||
voice_payload = TelegramVoice(
|
||||
file_id=msg.voice.file_id,
|
||||
mime_type=msg.voice.mime_type,
|
||||
file_size=msg.voice.file_size,
|
||||
duration=msg.voice.duration,
|
||||
raw=msgspec.to_builtins(msg.voice),
|
||||
)
|
||||
if raw_text is None and caption is None:
|
||||
text = ""
|
||||
document_payload: TelegramDocument | None = None
|
||||
document = msg.get("document")
|
||||
if isinstance(document, dict):
|
||||
document_payload = _parse_document_payload(document)
|
||||
if msg.document is not None:
|
||||
document_payload = _document_from_media(msg.document)
|
||||
if document_payload is None and msg.video is not None:
|
||||
document_payload = _document_from_media(msg.video)
|
||||
if document_payload is None:
|
||||
video = msg.get("video")
|
||||
if isinstance(video, dict):
|
||||
document_payload = _parse_document_payload(video)
|
||||
if document_payload is None:
|
||||
photo = msg.get("photo")
|
||||
if isinstance(photo, list):
|
||||
best: dict[str, Any] | None = None
|
||||
best_score = -1
|
||||
for item in photo:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
file_id = item.get("file_id")
|
||||
if not isinstance(file_id, str) or not file_id:
|
||||
continue
|
||||
size = item.get("file_size")
|
||||
if isinstance(size, int) and not isinstance(size, bool):
|
||||
score = size
|
||||
else:
|
||||
width = item.get("width")
|
||||
height = item.get("height")
|
||||
if isinstance(width, int) and isinstance(height, int):
|
||||
score = width * height
|
||||
else:
|
||||
score = 0
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best = item
|
||||
if best is not None:
|
||||
document_payload = _parse_document_payload(best)
|
||||
if document_payload is None and file_command:
|
||||
sticker = msg.get("sticker")
|
||||
if isinstance(sticker, dict):
|
||||
document_payload = _parse_document_payload(sticker)
|
||||
has_text = isinstance(raw_text, str) or isinstance(caption, str)
|
||||
best = _best_photo(msg.photo)
|
||||
if best is not None:
|
||||
document_payload = _document_from_photo(best)
|
||||
if document_payload is None and file_command and msg.sticker is not None:
|
||||
document_payload = _document_from_sticker(msg.sticker)
|
||||
has_text = raw_text is not None or caption is not None
|
||||
if not has_text and voice_payload is None and document_payload is None:
|
||||
return None
|
||||
chat = msg.get("chat")
|
||||
if not isinstance(chat, dict):
|
||||
return None
|
||||
msg_chat_id = chat.get("id")
|
||||
if not isinstance(msg_chat_id, int):
|
||||
return None
|
||||
chat_type = chat.get("type") if isinstance(chat.get("type"), str) else None
|
||||
is_forum = chat.get("is_forum")
|
||||
if not isinstance(is_forum, bool):
|
||||
is_forum = None
|
||||
msg_chat_id = msg.chat.id
|
||||
chat_type = msg.chat.type
|
||||
is_forum = msg.chat.is_forum
|
||||
allowed = chat_ids
|
||||
if allowed is None and chat_id is not None:
|
||||
allowed = {chat_id}
|
||||
if allowed is not None and msg_chat_id not in allowed:
|
||||
return None
|
||||
message_id = msg.get("message_id")
|
||||
if not isinstance(message_id, int):
|
||||
return None
|
||||
reply = msg.get("reply_to_message")
|
||||
reply_to_message_id = None
|
||||
reply_to_text = None
|
||||
reply_to_is_bot = None
|
||||
reply_to_username = None
|
||||
if isinstance(reply, dict):
|
||||
reply_to_message_id = (
|
||||
reply.get("message_id")
|
||||
if isinstance(reply.get("message_id"), int)
|
||||
else None
|
||||
)
|
||||
reply_to_text = (
|
||||
reply.get("text") if isinstance(reply.get("text"), str) else None
|
||||
)
|
||||
reply_from = reply.get("from")
|
||||
if isinstance(reply_from, dict):
|
||||
is_bot = reply_from.get("is_bot")
|
||||
if isinstance(is_bot, bool):
|
||||
reply_to_is_bot = is_bot
|
||||
username = reply_from.get("username")
|
||||
if isinstance(username, str):
|
||||
reply_to_username = username
|
||||
sender = msg.get("from")
|
||||
sender_id = (
|
||||
sender.get("id")
|
||||
if isinstance(sender, dict) and isinstance(sender.get("id"), int)
|
||||
else None
|
||||
reply = msg.reply_to_message
|
||||
reply_to_message_id = reply.message_id if reply is not None else None
|
||||
reply_to_text = reply.text if reply is not None else None
|
||||
reply_to_is_bot = (
|
||||
reply.from_.is_bot if reply is not None and reply.from_ is not None else None
|
||||
)
|
||||
media_group_id = msg.get("media_group_id")
|
||||
if not isinstance(media_group_id, str):
|
||||
media_group_id = None
|
||||
thread_id = msg.get("message_thread_id")
|
||||
if isinstance(thread_id, bool) or not isinstance(thread_id, int):
|
||||
thread_id = None
|
||||
is_topic_message = msg.get("is_topic_message")
|
||||
if not isinstance(is_topic_message, bool):
|
||||
is_topic_message = None
|
||||
reply_to_username = (
|
||||
reply.from_.username if reply is not None and reply.from_ is not None else None
|
||||
)
|
||||
sender_id = msg.from_.id if msg.from_ is not None else None
|
||||
media_group_id = msg.media_group_id
|
||||
thread_id = msg.message_thread_id
|
||||
is_topic_message = msg.is_topic_message
|
||||
return TelegramIncomingMessage(
|
||||
transport="telegram",
|
||||
chat_id=msg_chat_id,
|
||||
message_id=message_id,
|
||||
message_id=msg.message_id,
|
||||
text=text,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
reply_to_text=reply_to_text,
|
||||
@@ -218,51 +127,80 @@ def _parse_incoming_message(
|
||||
is_forum=is_forum,
|
||||
voice=voice_payload,
|
||||
document=document_payload,
|
||||
raw=msg,
|
||||
raw=msgspec.to_builtins(msg),
|
||||
)
|
||||
|
||||
|
||||
def _parse_callback_query(
|
||||
query: dict[str, Any],
|
||||
query: CallbackQuery,
|
||||
*,
|
||||
chat_id: int | None = None,
|
||||
chat_ids: set[int] | None = None,
|
||||
) -> TelegramCallbackQuery | None:
|
||||
callback_id = query.get("id")
|
||||
if not isinstance(callback_id, str) or not callback_id:
|
||||
return None
|
||||
msg = query.get("message")
|
||||
if not isinstance(msg, dict):
|
||||
return None
|
||||
chat = msg.get("chat")
|
||||
if not isinstance(chat, dict):
|
||||
return None
|
||||
msg_chat_id = chat.get("id")
|
||||
if not isinstance(msg_chat_id, int):
|
||||
callback_id = query.id
|
||||
msg = query.message
|
||||
if msg is None:
|
||||
return None
|
||||
msg_chat_id = msg.chat.id
|
||||
allowed = chat_ids
|
||||
if allowed is None and chat_id is not None:
|
||||
allowed = {chat_id}
|
||||
if allowed is not None and msg_chat_id not in allowed:
|
||||
return None
|
||||
message_id = msg.get("message_id")
|
||||
if not isinstance(message_id, int):
|
||||
return None
|
||||
data = query.get("data") if isinstance(query.get("data"), str) else None
|
||||
sender = query.get("from")
|
||||
sender_id = (
|
||||
sender.get("id")
|
||||
if isinstance(sender, dict) and isinstance(sender.get("id"), int)
|
||||
else None
|
||||
)
|
||||
data = query.data
|
||||
sender_id = query.from_.id if query.from_ is not None else None
|
||||
return TelegramCallbackQuery(
|
||||
transport="telegram",
|
||||
chat_id=msg_chat_id,
|
||||
message_id=message_id,
|
||||
message_id=msg.message_id,
|
||||
callback_query_id=callback_id,
|
||||
data=data,
|
||||
sender_id=sender_id,
|
||||
raw=query,
|
||||
raw=msgspec.to_builtins(query),
|
||||
)
|
||||
|
||||
|
||||
def _best_photo(photos: list[PhotoSize] | None) -> PhotoSize | None:
|
||||
if not photos:
|
||||
return None
|
||||
best = None
|
||||
best_score = -1
|
||||
for item in photos:
|
||||
size = item.file_size
|
||||
score = size if size is not None else item.width * item.height
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best = item
|
||||
return best
|
||||
|
||||
|
||||
def _document_from_media(media: Document | Video) -> TelegramDocument:
|
||||
return TelegramDocument(
|
||||
file_id=media.file_id,
|
||||
file_name=media.file_name,
|
||||
mime_type=media.mime_type,
|
||||
file_size=media.file_size,
|
||||
raw=msgspec.to_builtins(media),
|
||||
)
|
||||
|
||||
|
||||
def _document_from_photo(photo: PhotoSize) -> TelegramDocument:
|
||||
return TelegramDocument(
|
||||
file_id=photo.file_id,
|
||||
file_name=None,
|
||||
mime_type=None,
|
||||
file_size=photo.file_size,
|
||||
raw=msgspec.to_builtins(photo),
|
||||
)
|
||||
|
||||
|
||||
def _document_from_sticker(sticker: Sticker) -> TelegramDocument:
|
||||
return TelegramDocument(
|
||||
file_id=sticker.file_id,
|
||||
file_name=None,
|
||||
mime_type=None,
|
||||
file_size=sticker.file_size,
|
||||
raw=msgspec.to_builtins(sticker),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
],
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user