Merge pull request #1969 from coder2020official/botapi6.7

Bot API 6.7 - Not much to do, just minor improvements
This commit is contained in:
Badiboy 2023-04-24 09:38:21 +03:00 committed by GitHub
commit abec3dc60e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 348 additions and 31 deletions

View File

@ -10,7 +10,7 @@
<p align="center">A simple, but extensible Python implementation for the <a href="https://core.telegram.org/bots/api">Telegram Bot API</a>.</p>
<p align="center">Both synchronous and asynchronous.</p>
## <p align="center">Supported Bot API version: <a href="https://core.telegram.org/bots/api#march-9-2023">6.6</a>!
## <p align="center">Supported Bot API version: <a href="https://core.telegram.org/bots/api#april-21-2023">6.7</a>!
<h2><a href='https://pytba.readthedocs.io/en/latest/index.html'>Official documentation</a></h2>
<h2><a href='https://pytba.readthedocs.io/ru/latest/index.html'>Official ru documentation</a></h2>

View File

@ -3443,6 +3443,40 @@ class TeleBot:
"""
result = apihelper.get_my_commands(self.token, scope, language_code)
return [types.BotCommand.de_json(cmd) for cmd in result]
def set_my_name(self, name: Optional[str]=None, language_code: Optional[str]=None):
"""
Use this method to change the bot's name. Returns True on success.
Telegram documentation: https://core.telegram.org/bots/api#setmyname
:param name: Optional. New bot name; 0-64 characters. Pass an empty string to remove the dedicated name for the given language.
:type name: :obj:`str`
:param language_code: Optional. A two-letter ISO 639-1 language code. If empty, the name will be shown to all users for whose
language there is no dedicated name.
:type language_code: :obj:`str`
:return: True on success.
"""
return apihelper.set_my_name(self.token, name, language_code)
def get_my_name(self, language_code: Optional[str]=None):
"""
Use this method to get the current bot name for the given user language.
Returns BotName on success.
Telegram documentation: https://core.telegram.org/bots/api#getmyname
:param language_code: Optional. A two-letter ISO 639-1 language code or an empty string
:type language_code: :obj:`str`
:return: :class:`telebot.types.BotName`
"""
result = apihelper.get_my_name(self.token, language_code)
return types.BotName.de_json(result)
def set_my_description(self, description: Optional[str]=None, language_code: Optional[str]=None):
"""
@ -3450,6 +3484,8 @@ class TeleBot:
the chat with the bot if the chat is empty.
Returns True on success.
Telegram documentation: https://core.telegram.org/bots/api#setmydescription
:param description: New bot description; 0-512 characters. Pass an empty string to remove the dedicated description for the given language.
:type description: :obj:`str`
@ -3467,6 +3503,8 @@ class TeleBot:
Use this method to get the current bot description for the given user language.
Returns BotDescription on success.
Telegram documentation: https://core.telegram.org/bots/api#getmydescription
:param language_code: A two-letter ISO 639-1 language code or an empty string
:type language_code: :obj:`str`
@ -3481,6 +3519,8 @@ class TeleBot:
is sent together with the link when users share the bot.
Returns True on success.
Telegram documentation: https://core.telegram.org/bots/api#setmyshortdescription
:param short_description: New short description for the bot; 0-120 characters. Pass an empty string to remove the dedicated short description for the given language.
:type short_description: :obj:`str`
@ -3498,6 +3538,8 @@ class TeleBot:
Use this method to get the current bot short description for the given user language.
Returns BotShortDescription on success.
Telegram documentation: https://core.telegram.org/bots/api#getmyshortdescription
:param language_code: A two-letter ISO 639-1 language code or an empty string
:type language_code: :obj:`str`
@ -4479,7 +4521,8 @@ class TeleBot:
is_personal: Optional[bool]=None,
next_offset: Optional[str]=None,
switch_pm_text: Optional[str]=None,
switch_pm_parameter: Optional[str]=None) -> bool:
switch_pm_parameter: Optional[str]=None,
button: Optional[types.InlineQueryResultsButton]=None) -> bool:
"""
Use this method to send answers to an inline query. On success, True is returned.
No more than 50 results per query are allowed.
@ -4515,11 +4558,18 @@ class TeleBot:
:param switch_pm_text: Parameter for the start message sent to the bot when user presses the switch button
:type switch_pm_text: :obj:`str`
:param button: A JSON-serialized object describing a button to be shown above inline query results
:type button: :obj:`types.InlineQueryResultsButton`
:return: On success, True is returned.
:rtype: :obj:`bool`
"""
if not button and (switch_pm_text or switch_pm_parameter):
logger.warning("switch_pm_text and switch_pm_parameter are deprecated for answer_inline_query. Use button instead.")
button = types.InlineQueryResultsButton(text=switch_pm_text, start_parameter=switch_pm_parameter)
return apihelper.answer_inline_query(self.token, inline_query_id, results, cache_time, is_personal, next_offset,
switch_pm_text, switch_pm_parameter)
button)
def answer_callback_query(
self, callback_query_id: int,

View File

@ -1196,6 +1196,22 @@ def get_my_commands(token, scope=None, language_code=None):
payload['language_code'] = language_code
return _make_request(token, method_url, params=payload)
def set_my_name(token, name=None, language_code=None):
method_url = r'setMyName'
payload = {}
if name is not None:
payload['name'] = name
if language_code is not None:
payload['language_code'] = language_code
return _make_request(token, method_url, params=payload, method='post')
def get_my_name(token, language_code=None):
method_url = r'getMyName'
payload = {}
if language_code is not None:
payload['language_code'] = language_code
return _make_request(token, method_url, params=payload)
def set_chat_menu_button(token, chat_id=None, menu_button=None):
method_url = r'setChatMenuButton'
payload = {}
@ -1598,7 +1614,7 @@ def answer_callback_query(token, callback_query_id, text=None, show_alert=None,
def answer_inline_query(token, inline_query_id, results, cache_time=None, is_personal=None, next_offset=None,
switch_pm_text=None, switch_pm_parameter=None):
button=None):
method_url = 'answerInlineQuery'
payload = {'inline_query_id': inline_query_id, 'results': _convert_list_json_serializable(results)}
if cache_time is not None:
@ -1607,10 +1623,8 @@ def answer_inline_query(token, inline_query_id, results, cache_time=None, is_per
payload['is_personal'] = is_personal
if next_offset is not None:
payload['next_offset'] = next_offset
if switch_pm_text:
payload['switch_pm_text'] = switch_pm_text
if switch_pm_parameter:
payload['switch_pm_parameter'] = switch_pm_parameter
if button is not None:
payload["button"] = button.to_json()
return _make_request(token, method_url, params=payload, method='post')

View File

@ -4361,6 +4361,40 @@ class AsyncTeleBot:
result = await asyncio_helper.get_my_commands(self.token, scope, language_code)
return [types.BotCommand.de_json(cmd) for cmd in result]
async def set_my_name(self, name: Optional[str]=None, language_code: Optional[str]=None):
"""
Use this method to change the bot's name. Returns True on success.
Telegram documentation: https://core.telegram.org/bots/api#setmyname
:param name: Optional. New bot name; 0-64 characters. Pass an empty string to remove the dedicated name for the given language.
:type name: :obj:`str`
:param language_code: Optional. A two-letter ISO 639-1 language code. If empty, the name will be shown to all users for whose
language there is no dedicated name.
:type language_code: :obj:`str`
:return: True on success.
"""
return await asyncio_helper.set_my_name(self.token, name, language_code)
async def get_my_name(self, language_code: Optional[str]=None):
"""
Use this method to get the current bot name for the given user language.
Returns BotName on success.
Telegram documentation: https://core.telegram.org/bots/api#getmyname
:param language_code: Optional. A two-letter ISO 639-1 language code or an empty string
:type language_code: :obj:`str`
:return: :class:`telebot.types.BotName`
"""
result = await asyncio_helper.get_my_name(self.token, language_code)
return types.BotName.de_json(result)
async def set_chat_menu_button(self, chat_id: Union[int, str]=None,
menu_button: types.MenuButton=None) -> bool:
"""
@ -5337,7 +5371,8 @@ class AsyncTeleBot:
is_personal: Optional[bool]=None,
next_offset: Optional[str]=None,
switch_pm_text: Optional[str]=None,
switch_pm_parameter: Optional[str]=None) -> bool:
switch_pm_parameter: Optional[str]=None,
button: Optional[types.InlineQueryResultsButton]=None) -> bool:
"""
Use this method to send answers to an inline query. On success, True is returned.
No more than 50 results per query are allowed.
@ -5373,11 +5408,18 @@ class AsyncTeleBot:
:param switch_pm_text: Parameter for the start message sent to the bot when user presses the switch button
:type switch_pm_text: :obj:`str`
:param button: A JSON-serialized object describing a button to be shown above inline query results
:type button: :obj:`types.InlineQueryResultsButton`
:return: On success, True is returned.
:rtype: :obj:`bool`
"""
if not button and (switch_pm_text or switch_pm_parameter):
logger.warning("switch_pm_text and switch_pm_parameter are deprecated for answer_inline_query. Use button instead.")
button = types.InlineQueryResultsButton(text=switch_pm_text, start_parameter=switch_pm_parameter)
return await asyncio_helper.answer_inline_query(self.token, inline_query_id, results, cache_time, is_personal, next_offset,
switch_pm_text, switch_pm_parameter)
button)
async def answer_callback_query(
self, callback_query_id: int,

View File

@ -1183,6 +1183,23 @@ async def get_my_commands(token, scope=None, language_code=None):
payload['language_code'] = language_code
return await _process_request(token, method_url, params=payload)
async def set_my_name(token, name=None, language_code=None):
method_url = r'setMyName'
payload = {}
if name is not None:
payload['name'] = name
if language_code is not None:
payload['language_code'] = language_code
return await _process_request(token, method_url, params=payload, method='post')
async def get_my_name(token, language_code=None):
method_url = r'getMyName'
payload = {}
if language_code is not None:
payload['language_code'] = language_code
return await _process_request(token, method_url, params=payload)
async def set_chat_menu_button(token, chat_id=None, menu_button=None):
method_url = r'setChatMenuButton'
payload = {}
@ -1587,7 +1604,7 @@ async def answer_callback_query(token, callback_query_id, text=None, show_alert=
async def answer_inline_query(token, inline_query_id, results, cache_time=None, is_personal=None, next_offset=None,
switch_pm_text=None, switch_pm_parameter=None):
button=None):
method_url = 'answerInlineQuery'
payload = {'inline_query_id': inline_query_id, 'results': await _convert_list_json_serializable(results)}
if cache_time is not None:
@ -1596,10 +1613,10 @@ async def answer_inline_query(token, inline_query_id, results, cache_time=None,
payload['is_personal'] = is_personal
if next_offset is not None:
payload['next_offset'] = next_offset
if switch_pm_text:
payload['switch_pm_text'] = switch_pm_text
if switch_pm_parameter:
payload['switch_pm_parameter'] = switch_pm_parameter
if button is not None:
payload["button"] = button.to_json()
return await _process_request(token, method_url, params=payload, method='post')

View File

@ -237,6 +237,9 @@ class ChatMemberUpdated(JsonDeserializable):
link events only.
:type invite_link: :class:`telebot.types.ChatInviteLink`
:param via_chat_folder_invite_link: Optional. True, if the user joined the chat via a chat folder invite link
:type via_chat_folder_invite_link: :obj:`bool`
:return: Instance of the class
:rtype: :class:`telebot.types.ChatMemberUpdated`
"""
@ -251,13 +254,15 @@ class ChatMemberUpdated(JsonDeserializable):
obj['invite_link'] = ChatInviteLink.de_json(obj.get('invite_link'))
return cls(**obj)
def __init__(self, chat, from_user, date, old_chat_member, new_chat_member, invite_link=None, **kwargs):
def __init__(self, chat, from_user, date, old_chat_member, new_chat_member, invite_link=None, via_chat_folder_invite_link=None,
**kwargs):
self.chat: Chat = chat
self.from_user: User = from_user
self.date: int = date
self.old_chat_member: ChatMember = old_chat_member
self.new_chat_member: ChatMember = new_chat_member
self.invite_link: Optional[ChatInviteLink] = invite_link
self.via_chat_folder_invite_link: Optional[bool] = via_chat_folder_invite_link
@property
def difference(self) -> Dict[str, List]:
@ -1308,6 +1313,7 @@ class Message(JsonDeserializable):
"strikethrough": "<s>{text}</s>",
"underline": "<u>{text}</u>",
"spoiler": "<span class=\"tg-spoiler\">{text}</span>",
"custom_emoji": "<tg-emoji emoji-id=\"{custom_emoji_id}\">{text}</tg-emoji>"
}
if hasattr(self, "custom_subs"):
@ -1316,7 +1322,7 @@ class Message(JsonDeserializable):
utf16_text = text.encode("utf-16-le")
html_text = ""
def func(upd_text, subst_type=None, url=None, user=None):
def func(upd_text, subst_type=None, url=None, user=None, custom_emoji_id=None):
upd_text = upd_text.decode("utf-16-le")
if subst_type == "text_mention":
subst_type = "text_link"
@ -1327,30 +1333,41 @@ class Message(JsonDeserializable):
if not subst_type or not _subs.get(subst_type):
return upd_text
subs = _subs.get(subst_type)
if subst_type == "custom_emoji":
return subs.format(text=upd_text, custom_emoji_id=custom_emoji_id)
return subs.format(text=upd_text, url=url)
offset = 0
start_index = 0
end_index = 0
for entity in entities:
if entity.offset > offset:
# when the offset is not 0: for example, a __b__
# we need to add the text before the entity to the html_text
html_text += func(utf16_text[offset * 2 : entity.offset * 2])
offset = entity.offset
html_text += func(utf16_text[offset * 2 : (offset + entity.length) * 2], entity.type, entity.url, entity.user)
new_string = func(utf16_text[offset * 2 : (offset + entity.length) * 2], entity.type, entity.url, entity.user, entity.custom_emoji_id)
start_index = len(html_text)
html_text += new_string
offset += entity.length
end_index = len(html_text)
elif entity.offset == offset:
html_text += func(utf16_text[offset * 2 : (offset + entity.length) * 2], entity.type, entity.url, entity.user)
new_string = func(utf16_text[offset * 2 : (offset + entity.length) * 2], entity.type, entity.url, entity.user, entity.custom_emoji_id)
start_index = len(html_text)
html_text += new_string
end_index = len(html_text)
offset += entity.length
else:
# Here we are processing nested entities.
# We shouldn't update offset, because they are the same as entity before.
# And, here we are replacing previous string with a new html-rendered text(previous string is already html-rendered,
# And we don't change it).
entity_string = utf16_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
formatted_string = func(entity_string, entity.type, entity.url, entity.user)
entity_string_decoded = entity_string.decode("utf-16-le")
last_occurence = html_text.rfind(entity_string_decoded)
string_length = len(entity_string_decoded)
#html_text = html_text.replace(html_text[last_occurence:last_occurence+string_length], formatted_string)
html_text = html_text[:last_occurence] + formatted_string + html_text[last_occurence+string_length:]
entity_string = html_text[start_index : end_index].encode("utf-16-le")
formatted_string = func(entity_string, entity.type, entity.url, entity.user, entity.custom_emoji_id).replace("&amp;", "&").replace("&lt;", "<").replace("&gt;",">")
html_text = html_text[:start_index] + formatted_string + html_text[end_index:]
end_index = len(html_text)
if offset * 2 < len(utf16_text):
html_text += func(utf16_text[offset * 2:])
@ -2592,6 +2609,10 @@ class InlineKeyboardButton(Dictionaryable, JsonSerializable, JsonDeserializable)
something from multiple options.
:type switch_inline_query_current_chat: :obj:`str`
:param switch_inline_query_chosen_chat: Optional. If set, pressing the button will prompt the user to select one of their chats of the
specified type, open that chat and insert the bot's username and the specified inline query in the input field
:type switch_inline_query_chosen_chat: :class:`telebot.types.SwitchInlineQueryChosenChat`
:param callback_game: Optional. Description of the game that will be launched when the user presses the
button. NOTE: This type of button must always be the first button in the first row.
:type callback_game: :class:`telebot.types.CallbackGame`
@ -2611,17 +2632,20 @@ class InlineKeyboardButton(Dictionaryable, JsonSerializable, JsonDeserializable)
obj['login_url'] = LoginUrl.de_json(obj.get('login_url'))
if 'web_app' in obj:
obj['web_app'] = WebAppInfo.de_json(obj.get('web_app'))
if 'switch_inline_query_chosen_chat' in obj:
obj['switch_inline_query_chosen_chat'] = SwitchInlineQueryChosenChat.de_json(obj.get('switch_inline_query_chosen_chat'))
return cls(**obj)
def __init__(self, text, url=None, callback_data=None, web_app=None, switch_inline_query=None,
switch_inline_query_current_chat=None, callback_game=None, pay=None, login_url=None, **kwargs):
switch_inline_query_current_chat=None, switch_inline_query_chosen_chat=None, callback_game=None, pay=None, login_url=None, **kwargs):
self.text: str = text
self.url: str = url
self.callback_data: str = callback_data
self.web_app: WebAppInfo = web_app
self.switch_inline_query: str = switch_inline_query
self.switch_inline_query_current_chat: str = switch_inline_query_current_chat
self.switch_inline_query_chosen_chat: SwitchInlineQueryChosenChat = switch_inline_query_chosen_chat
self.callback_game = callback_game # Not Implemented
self.pay: bool = pay
self.login_url: LoginUrl = login_url
@ -2647,6 +2671,8 @@ class InlineKeyboardButton(Dictionaryable, JsonSerializable, JsonDeserializable)
json_dict['pay'] = self.pay
if self.login_url is not None:
json_dict['login_url'] = self.login_url.to_dict()
if self.switch_inline_query_chosen_chat is not None:
json_dict['switch_inline_query_chosen_chat'] = self.switch_inline_query_chosen_chat.to_dict()
return json_dict
@ -7396,13 +7422,20 @@ class WriteAccessAllowed(JsonDeserializable):
Currently holds no information.
Telegram documentation: https://core.telegram.org/bots/api#writeaccessallowed
:param web_app_name: Optional. Name of the Web App which was launched from a link
:type web_app_name: :obj:`str`
"""
@classmethod
def de_json(cls, json_string):
return cls()
if json_string is None: return None
obj = cls.check_json(json_string)
return cls(**obj)
def __init__(self) -> None:
pass
def __init__(self, web_app_name: str) -> None:
self.web_app_name: str = web_app_name
class UserShared(JsonDeserializable):
@ -7576,4 +7609,137 @@ class InputSticker(Dictionaryable, JsonSerializable):
return self.to_json(), {self._sticker_name: self.sticker}
class SwitchInlineQueryChosenChat(JsonDeserializable, Dictionaryable, JsonSerializable):
"""
Represents an inline button that switches the current user to inline mode in a chosen chat,
with an optional default inline query.
Telegram Documentation: https://core.telegram.org/bots/api#inlinekeyboardbutton
:param query: Optional. The default inline query to be inserted in the input field.
If left empty, only the bot's username will be inserted
:type query: :obj:`str`
:param allow_user_chats: Optional. True, if private chats with users can be chosen
:type allow_user_chats: :obj:`bool`
:param allow_bot_chats: Optional. True, if private chats with bots can be chosen
:type allow_bot_chats: :obj:`bool`
:param allow_group_chats: Optional. True, if group and supergroup chats can be chosen
:type allow_group_chats: :obj:`bool`
:param allow_channel_chats: Optional. True, if channel chats can be chosen
:type allow_channel_chats: :obj:`bool`
:return: Instance of the class
:rtype: :class:`SwitchInlineQueryChosenChat`
"""
@classmethod
def de_json(cls, json_string):
if json_string is None:
return None
obj = cls.check_json(json_string)
return cls(**obj)
def __init__(self, query=None, allow_user_chats=None, allow_bot_chats=None, allow_group_chats=None,
allow_channel_chats=None):
self.query: str = query
self.allow_user_chats: bool = allow_user_chats
self.allow_bot_chats: bool = allow_bot_chats
self.allow_group_chats: bool = allow_group_chats
self.allow_channel_chats: bool = allow_channel_chats
def to_dict(self):
json_dict = {}
if self.query is not None:
json_dict['query'] = self.query
if self.allow_user_chats is not None:
json_dict['allow_user_chats'] = self.allow_user_chats
if self.allow_bot_chats is not None:
json_dict['allow_bot_chats'] = self.allow_bot_chats
if self.allow_group_chats is not None:
json_dict['allow_group_chats'] = self.allow_group_chats
if self.allow_channel_chats is not None:
json_dict['allow_channel_chats'] = self.allow_channel_chats
return json_dict
def to_json(self):
return json.dumps(self.to_dict())
class BotName(JsonDeserializable):
"""
This object represents a bot name.
Telegram Documentation: https://core.telegram.org/bots/api#botname
:param name: The bot name
:type name: :obj:`str`
:return: Instance of the class
:rtype: :class:`BotName`
"""
@classmethod
def de_json(cls, json_string):
if json_string is None:
return None
obj = cls.check_json(json_string)
return cls(**obj)
def __init__(self, name: str):
self.name: str = name
class InlineQueryResultsButton(JsonSerializable, Dictionaryable):
"""
This object represents a button to be shown above inline query results.
You must use exactly one of the optional fields.
Telegram documentation: https://core.telegram.org/bots/api#inlinequeryresultsbutton
:param text: Label text on the button
:type text: :obj:`str`
:param web_app: Optional. Description of the Web App that will be launched when the user presses the button.
The Web App will be able to switch back to the inline mode using the method web_app_switch_inline_query inside the Web App.
:type web_app: :class:`telebot.types.WebAppInfo`
:param start_parameter: Optional. Deep-linking parameter for the /start message sent to the bot when a user presses the button.
1-64 characters, only A-Z, a-z, 0-9, _ and - are allowed.
Example: An inline bot that sends YouTube videos can ask the user to connect the bot to their YouTube account to adapt search
results accordingly. To do this, it displays a 'Connect your YouTube account' button above the results, or even before showing
any. The user presses the button, switches to a private chat with the bot and, in doing so, passes a start parameter that instructs
the bot to return an OAuth link. Once done, the bot can offer a switch_inline button so that the user can easily return to the chat
where they wanted to use the bot's inline capabilities.
:type start_parameter: :obj:`str`
:return: Instance of the class
:rtype: :class:`InlineQueryResultsButton`
"""
def __init__(self, text: str, web_app: Optional[WebAppInfo]=None, start_parameter: Optional[str]=None) -> None:
self.text: str = text
self.web_app: Optional[WebAppInfo] = web_app
self.start_parameter: Optional[str] = start_parameter
def to_dict(self) -> dict:
json_dict = {
'text': self.text
}
if self.web_app is not None:
json_dict['web_app'] = self.web_app.to_dict()
if self.start_parameter is not None:
json_dict['start_parameter'] = self.start_parameter
return json_dict
def to_json(self) -> str:
return json.dumps(self.to_dict())

View File

@ -271,5 +271,33 @@ def test_sent_web_app_message():
assert sent_web_app_message.inline_message_id == '29430'
def test_message_entity():
# TODO: Add support for nesting entities
sample_string_1 = r'{"update_id":934522126,"message":{"message_id":1374510,"from":{"id":927266710,"is_bot":false,"first_name":">_run","username":"coder2020","language_code":"en","is_premium":true},"chat":{"id":927266710,"first_name":">_run","username":"coder2020","type":"private"},"date":1682177590,"text":"b b b","entities":[{"offset":0,"length":2,"type":"bold"},{"offset":0,"length":1,"type":"italic"},{"offset":2,"length":2,"type":"bold"},{"offset":2,"length":1,"type":"italic"},{"offset":4,"length":1,"type":"bold"},{"offset":4,"length":1,"type":"italic"}]}}'
update = types.Update.de_json(sample_string_1)
message: types.Message = update.message
assert message.html_text == "<i><b>b </b></i><i><b>b </b></i><i><b>b</b></i>"
sample_string_2 = r'{"update_id":934522166,"message":{"message_id":1374526,"from":{"id":927266710,"is_bot":false,"first_name":">_run","username":"coder2020","language_code":"en","is_premium":true},"chat":{"id":927266710,"first_name":">_run","username":"coder2020","type":"private"},"date":1682179716,"text":"b b b","entities":[{"offset":0,"length":1,"type":"bold"},{"offset":2,"length":1,"type":"bold"},{"offset":4,"length":1,"type":"italic"}]}}'
message_2 = types.Update.de_json(sample_string_2).message
assert message_2.html_text == "<b>b</b> <b>b</b> <i>b</i>"
sample_string_3 = r'{"update_id":934522172,"message":{"message_id":1374530,"from":{"id":927266710,"is_bot":false,"first_name":">_run","username":"coder2020","language_code":"en","is_premium":true},"chat":{"id":927266710,"first_name":">_run","username":"coder2020","type":"private"},"date":1682179968,"text":"This is a bold text with a nested italic and bold text.","entities":[{"offset":10,"length":4,"type":"bold"},{"offset":27,"length":7,"type":"italic"},{"offset":34,"length":15,"type":"bold"},{"offset":34,"length":15,"type":"italic"}]}}'
message_3 = types.Update.de_json(sample_string_3).message
assert message_3.html_text == "This is a <b>bold</b> text with a <i>nested </i><i><b>italic and bold</b></i> text."
sample_string_4 = r'{"update_id":934522437,"message":{"message_id":1374619,"from":{"id":927266710,"is_bot":false,"first_name":">_run","username":"coder2020","language_code":"en","is_premium":true},"chat":{"id":927266710,"first_name":">_run","username":"coder2020","type":"private"},"date":1682189507,"forward_from":{"id":927266710,"is_bot":false,"first_name":">_run","username":"coder2020","language_code":"en","is_premium":true},"forward_date":1682189124,"text":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa😋😋","entities":[{"offset":0,"length":76,"type":"bold"},{"offset":0,"length":76,"type":"italic"},{"offset":0,"length":76,"type":"underline"},{"offset":0,"length":76,"type":"strikethrough"},{"offset":76,"length":2,"type":"custom_emoji","custom_emoji_id":"5456188142006575553"},{"offset":78,"length":2,"type":"custom_emoji","custom_emoji_id":"5456188142006575553"}]}}'
message_4 = types.Update.de_json(sample_string_4).message
assert message_4.html_text == '<s><u><i><b>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</b></i></u></s><tg-emoji emoji-id="5456188142006575553">😋</tg-emoji><tg-emoji emoji-id="5456188142006575553">😋</tg-emoji>'