Skip to content
Merged
Show file tree
Hide file tree
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
14 changes: 14 additions & 0 deletions telegram/_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6833,6 +6833,8 @@ async def send_poll(
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
question_entities: Optional[Sequence["MessageEntity"]] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
Expand Down Expand Up @@ -6917,6 +6919,16 @@ async def send_poll(
business_connection_id (:obj:`str`, optional): |business_id_str|

.. versionadded:: 21.1
question_parse_mode (:obj:`str`, optional): Mode for parsing entities in the question.
See the constants in :class:`telegram.constants.ParseMode` for the available modes.
Currently, only custom emoji entities are allowed.

.. versionadded:: NEXT.VERSION
question_entities (Sequence[:class:`telegram.Message`], optional): Special entities
that appear in the poll :paramref:`question`. It can be specified instead of
:paramref:`question_parse_mode`.

.. versionadded:: NEXT.VERSION

Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
Expand Down Expand Up @@ -6962,6 +6974,8 @@ async def send_poll(
"explanation_entities": explanation_entities,
"open_period": open_period,
"close_date": close_date,
"question_parse_mode": question_parse_mode,
"question_entities": question_entities,
}

return await self._send_message(
Expand Down
4 changes: 4 additions & 0 deletions telegram/_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2903,6 +2903,8 @@ async def send_poll(
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
question_entities: Optional[Sequence["MessageEntity"]] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
Expand Down Expand Up @@ -2949,6 +2951,8 @@ async def send_poll(
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
question_parse_mode=question_parse_mode,
question_entities=question_entities,
)

async def send_copy(
Expand Down
29 changes: 9 additions & 20 deletions telegram/_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
from telegram._utils.entities import parse_message_entities, parse_message_entity
from telegram._utils.types import (
CorrectOptionID,
FileInput,
Expand Down Expand Up @@ -2922,6 +2923,8 @@ async def reply_poll(
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: ODVInput[int] = DEFAULT_NONE,
reply_parameters: Optional["ReplyParameters"] = None,
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
question_entities: Optional[Sequence["MessageEntity"]] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
Expand Down Expand Up @@ -2992,6 +2995,8 @@ async def reply_poll(
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=self.business_connection_id,
question_parse_mode=question_parse_mode,
question_entities=question_entities,
)

async def reply_dice(
Expand Down Expand Up @@ -4202,9 +4207,7 @@ def parse_entity(self, entity: MessageEntity) -> str:
if not self.text:
raise RuntimeError("This Message has no 'text'.")

entity_text = self.text.encode("utf-16-le")
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
return entity_text.decode("utf-16-le")
return parse_message_entity(self.text, entity)

def parse_caption_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.
Expand All @@ -4228,9 +4231,7 @@ def parse_caption_entity(self, entity: MessageEntity) -> str:
if not self.caption:
raise RuntimeError("This Message has no 'caption'.")

entity_text = self.caption.encode("utf-16-le")
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
return entity_text.decode("utf-16-le")
return parse_message_entity(self.caption, entity)

def parse_entities(self, types: Optional[List[str]] = None) -> Dict[MessageEntity, str]:
"""
Expand All @@ -4255,12 +4256,7 @@ def parse_entities(self, types: Optional[List[str]] = None) -> Dict[MessageEntit
the text that belongs to them, calculated based on UTF-16 codepoints.

"""
if types is None:
types = MessageEntity.ALL_TYPES

return {
entity: self.parse_entity(entity) for entity in self.entities if entity.type in types
}
return parse_message_entities(self.text, self.entities, types=types)

def parse_caption_entities(
self, types: Optional[List[str]] = None
Expand All @@ -4287,14 +4283,7 @@ def parse_caption_entities(
the text that belongs to them, calculated based on UTF-16 codepoints.

"""
if types is None:
types = MessageEntity.ALL_TYPES

return {
entity: self.parse_caption_entity(entity)
for entity in self.caption_entities
if entity.type in types
}
return parse_message_entities(self.caption, self.caption_entities, types=types)

@classmethod
def _parse_html(
Expand Down
167 changes: 151 additions & 16 deletions telegram/_poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.entities import parse_message_entities, parse_message_entity
from telegram._utils.types import JSONDict, ODVInput

if TYPE_CHECKING:
Expand Down Expand Up @@ -113,26 +114,101 @@ class PollOption(TelegramObject):
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
characters.
voter_count (:obj:`int`): Number of users that voted for this option.
text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities
that appear in the option text. Currently, only custom emoji entities are allowed in
poll option texts.

.. versionadded:: NEXT.VERSION

Attributes:
text (:obj:`str`): Option text,
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
characters.
voter_count (:obj:`int`): Number of users that voted for this option.
text_entities (Tuple[:class:`telegram.MessageEntity`]): Special entities
that appear in the option text. Currently, only custom emoji entities are allowed in
poll option texts.
This list is empty if the question does not contain entities.

.. versionadded:: NEXT.VERSION

"""

__slots__ = ("text", "voter_count")
__slots__ = ("text", "text_entities", "voter_count")

def __init__(self, text: str, voter_count: int, *, api_kwargs: Optional[JSONDict] = None):
def __init__(
self,
text: str,
voter_count: int,
text_entities: Optional[Sequence[MessageEntity]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.text: str = text
self.voter_count: int = voter_count
self.text_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)

self._id_attrs = (self.text, self.voter_count)

self._freeze()

@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PollOption"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)

if not data:
return None

data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot)

return super().de_json(data=data, bot=bot)

def parse_entity(self, entity: MessageEntity) -> str:
"""Returns the text in :attr:`text`
from a given :class:`telegram.MessageEntity` of :attr:`text_entities`.

Note:
This method is present because Telegram calculates the offset and length in
UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
(That is, you can't just slice ``Message.text`` with the offset and length.)

.. versionadded:: NEXT.VERSION

Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to :attr:`text_entities`.

Returns:
:obj:`str`: The text of the given entity.
"""
return parse_message_entity(self.text, entity)

def parse_entities(self, types: Optional[List[str]] = None) -> Dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this polls question filtered by their ``type`` attribute as
the key, and the text that each entity belongs to as the value of the :obj:`dict`.

Note:
This method should always be used instead of the :attr:`text_entities`
attribute, since it calculates the correct substring from the message text based on
UTF-16 codepoints. See :attr:`parse_entity` for more info.

.. versionadded:: NEXT.VERSION

Args:
types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
``type`` attribute of an entity is contained in this list, it will be returned.
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.

Returns:
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
return parse_message_entities(self.text, self.text_entities, types)

MIN_LENGTH: Final[int] = constants.PollLimit.MIN_OPTION_LENGTH
""":const:`telegram.constants.PollLimit.MIN_OPTION_LENGTH`

Expand Down Expand Up @@ -282,6 +358,11 @@ class Poll(TelegramObject):

.. versionchanged:: 20.3
|datetime_localization|
question_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities
that appear in the :attr:`question`. Currently, only custom emoji entities are allowed
in poll questions.

.. versionadded:: NEXT.VERSION

Attributes:
id (:obj:`str`): Unique poll identifier.
Expand Down Expand Up @@ -318,6 +399,12 @@ class Poll(TelegramObject):

.. versionchanged:: 20.3
|datetime_localization|
question_entities (Tuple[:class:`telegram.MessageEntity`]): Special entities
that appear in the :attr:`question`. Currently, only custom emoji entities are allowed
in poll questions.
This list is empty if the question does not contain entities.

.. versionadded:: NEXT.VERSION

"""

Expand All @@ -333,6 +420,7 @@ class Poll(TelegramObject):
"open_period",
"options",
"question",
"question_entities",
"total_voter_count",
"type",
)
Expand All @@ -352,6 +440,7 @@ def __init__(
explanation_entities: Optional[Sequence[MessageEntity]] = None,
open_period: Optional[int] = None,
close_date: Optional[datetime.datetime] = None,
question_entities: Optional[Sequence[MessageEntity]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
Expand All @@ -371,6 +460,7 @@ def __init__(
)
self.open_period: Optional[int] = open_period
self.close_date: Optional[datetime.datetime] = close_date
self.question_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(question_entities)

self._id_attrs = (self.id,)

Expand All @@ -390,11 +480,13 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Poll"]:
data["options"] = [PollOption.de_json(option, bot) for option in data["options"]]
data["explanation_entities"] = MessageEntity.de_list(data.get("explanation_entities"), bot)
data["close_date"] = from_timestamp(data.get("close_date"), tzinfo=loc_tzinfo)
data["question_entities"] = MessageEntity.de_list(data.get("question_entities"), bot)

return super().de_json(data=data, bot=bot)

def parse_explanation_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.
"""Returns the text in :attr:`explanation` from a given :class:`telegram.MessageEntity` of
:attr:`explanation_entities`.

Note:
This method is present because Telegram calculates the offset and length in
Expand All @@ -403,7 +495,7 @@ def parse_explanation_entity(self, entity: MessageEntity) -> str:

Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to this message.
be an entity that belongs to :attr:`explanation_entities`.

Returns:
:obj:`str`: The text of the given entity.
Expand All @@ -415,10 +507,7 @@ def parse_explanation_entity(self, entity: MessageEntity) -> str:
if not self.explanation:
raise RuntimeError("This Poll has no 'explanation'.")

entity_text = self.explanation.encode("utf-16-le")
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]

return entity_text.decode("utf-16-le")
return parse_message_entity(self.explanation, entity)

def parse_explanation_entities(
self, types: Optional[List[str]] = None
Expand All @@ -442,15 +531,61 @@ def parse_explanation_entities(
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.

Raises:
RuntimeError: If the poll has no explanation.

"""
if not self.explanation:
raise RuntimeError("This Poll has no 'explanation'.")

return parse_message_entities(self.explanation, self.explanation_entities, types)

def parse_question_entity(self, entity: MessageEntity) -> str:
"""Returns the text in :attr:`question` from a given :class:`telegram.MessageEntity` of
:attr:`question_entities`.

.. versionadded:: NEXT.VERSION

Note:
This method is present because Telegram calculates the offset and length in
UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
(That is, you can't just slice ``Message.text`` with the offset and length.)

Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to :attr:`question_entities`.

Returns:
:obj:`str`: The text of the given entity.
"""
return parse_message_entity(self.question, entity)

def parse_question_entities(
self, types: Optional[List[str]] = None
) -> Dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this polls question filtered by their ``type`` attribute as
the key, and the text that each entity belongs to as the value of the :obj:`dict`.

.. versionadded:: NEXT.VERSION

Note:
This method should always be used instead of the :attr:`question_entities`
attribute, since it calculates the correct substring from the message text based on
UTF-16 codepoints. See :attr:`parse_question_entity` for more info.

Args:
types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
``type`` attribute of an entity is contained in this list, it will be returned.
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.

Returns:
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.

"""
if types is None:
types = MessageEntity.ALL_TYPES

return {
entity: self.parse_explanation_entity(entity)
for entity in self.explanation_entities
if entity.type in types
}
return parse_message_entities(self.question, self.question_entities, types)

REGULAR: Final[str] = constants.PollType.REGULAR
""":const:`telegram.constants.PollType.REGULAR`"""
Expand Down
Loading