Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 95 additions & 8 deletions telegram/_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Message."""
import datetime
import re
from html import escape
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Sequence, Tuple, Union
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Sequence, Tuple, TypedDict, Union

from telegram._chat import Chat
from telegram._dice import Dice
Expand Down Expand Up @@ -53,6 +54,7 @@
from telegram._payment.successfulpayment import SuccessfulPayment
from telegram._poll import Poll
from telegram._proximityalerttriggered import ProximityAlertTriggered
from telegram._reply import ReplyParameters
from telegram._shared import ChatShared, UserShared, UsersShared
from telegram._story import Story
from telegram._telegramobject import TelegramObject
Expand Down Expand Up @@ -106,6 +108,11 @@
)


class _ReplyKwargs(TypedDict):
chat_id: Union[str, int]
reply_parameters: ReplyParameters


class MaybeInaccessibleMessage(TelegramObject):
"""Base class for Telegram Message Objects.

Expand Down Expand Up @@ -1324,14 +1331,16 @@ def effective_attachment(

return self._effective_attachment # type: ignore[return-value]

def _quote(self, quote: Optional[bool], reply_to_message_id: Optional[int]) -> Optional[int]:
def _quote(
self, quote: Optional[bool], reply_to_message_id: Optional[int] = None
) -> Optional[ReplyParameters]:
"""Modify kwargs for replying with or without quoting."""
if reply_to_message_id is not None:
return reply_to_message_id
return ReplyParameters(reply_to_message_id)

if quote is not None:
if quote:
return self.message_id
return ReplyParameters(self.message_id)

else:
# Unfortunately we need some ExtBot logic here because it's hard to move shortcut
Expand All @@ -1341,10 +1350,85 @@ def _quote(self, quote: Optional[bool], reply_to_message_id: Optional[int]) -> O
else:
default_quote = None
if (default_quote is None and self.chat.type != Chat.PRIVATE) or default_quote:
return self.message_id
return ReplyParameters(self.message_id)

return None

def compute_quote_position_and_entities(
self, quote: str, index: Optional[int] = None
) -> Tuple[int, Tuple[MessageEntity, ...]]:
if not (text := (self.text or self.caption)):
raise RuntimeError("This message has neither text nor caption.")

effective_index = 0 or index

matches = list(re.finditer(re.escape(quote), text))
if (length := len(matches)) < effective_index:
raise ValueError(
f"You requested the {index}-nth occurrence of '{quote}', but this text appears "
f"only {length} times."
)
# apply utf-16 conversion
position = matches[index].start()
end_position = position + len(quote)
entities = tuple(
entity
for entity in self.entities or self.caption_entities
if position <= entity.offset <= end_position
)
return position, entities

def build_reply_arguments(
self,
quote: Optional[str] = None,
quote_index: Optional[int] = None,
target_chat_id: Optional[Union[int, str]] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
) -> _ReplyKwargs:
kwargs: _ReplyKwargs = {"chat_id": target_chat_id or self.chat_id}
target_chat_is_self = target_chat_id in (None, self.chat_id, f"@{self.username}")

if target_chat_is_self and message_thread_id in (
None,
self.message_thread_id,
):
# defaults handling will take place in `Bot._insert_defaults`
effective_aswr: ODVInput[bool] = allow_sending_without_reply
else:
effective_aswr = None

quote_position, quote_entities = (
self.compute_quote_position_and_entities(quote, quote_index) if quote else (None, None)
)
kwargs["reply_parameters"] = ReplyParameters(
chat_id=None if target_chat_is_self else self.chat_id,
message_id=self.message_id,
quote_position=quote_position,
quote_entities=quote_entities,
allow_sending_without_reply=effective_aswr,
)

return kwargs

async def _parse_quote_arguments(
self,
do_quote: Optional[Union[bool, _ReplyKwargs]],
quote: Optional[bool],
reply_to_message_id: Optional[int],
) -> Tuple[Union[str, int], ReplyParameters]:
if quote and do_quote:
raise ValueError("Mutually exclusive")
effective_do_quote = quote or do_quote
bool_do_quote = isinstance(effective_do_quote, bool)
reply_parameters = (
self._quote(quote, reply_to_message_id)
if bool_do_quote
else effective_do_quote["reply_parameters"]
)
chat_id = self.chat_id if bool_do_quote else effective_do_quote["chat_id"]
return chat_id, reply_parameters

async def reply_text(
self,
text: str,
Expand All @@ -1359,6 +1443,7 @@ async def reply_text(
message_thread_id: Optional[int] = None,
*,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
Expand All @@ -1380,14 +1465,15 @@ async def reply_text(
:class:`telegram.Message`: On success, instance representing the message posted.

"""
reply_to_message_id = self._quote(quote, reply_to_message_id)
chat_id, reply_parameters = await self._parse_quote_arguments(
do_quote, quote, reply_to_message_id
)
return await self.get_bot().send_message(
chat_id=self.chat_id,
chat_id=chat_id,
text=text,
parse_mode=parse_mode,
disable_web_page_preview=disable_web_page_preview,
disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id,
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
entities=entities,
Expand All @@ -1398,6 +1484,7 @@ async def reply_text(
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
reply_parameters=reply_parameters,
)

async def reply_markdown(
Expand Down