mirror of
https://github.com/eternnoir/pyTelegramBotAPI.git
synced 2023-08-10 21:12:57 +03:00
Timeouts in making requests are rethought
This commit is contained in:
parent
beb5a456eb
commit
2578e48134
@ -1,7 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ujson as json
|
import ujson as json
|
||||||
@ -12,6 +11,7 @@ import requests
|
|||||||
from requests.exceptions import HTTPError, ConnectionError, Timeout
|
from requests.exceptions import HTTPError, ConnectionError, Timeout
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
from requests.packages.urllib3 import fields
|
from requests.packages.urllib3 import fields
|
||||||
format_header_param = fields.format_header_param
|
format_header_param = fields.format_header_param
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -28,8 +28,11 @@ session = None
|
|||||||
API_URL = None
|
API_URL = None
|
||||||
FILE_URL = None
|
FILE_URL = None
|
||||||
|
|
||||||
CONNECT_TIMEOUT = 3.5
|
CONNECT_TIMEOUT = 15
|
||||||
READ_TIMEOUT = 9999
|
READ_TIMEOUT = 30
|
||||||
|
|
||||||
|
LONG_POLLING_TIMEOUT = 10 # Should be positive, short polling should be used for testing purposes only (https://core.telegram.org/bots/api#getupdates)
|
||||||
|
|
||||||
SESSION_TIME_TO_LIVE = None # In seconds. None - live forever, 0 - one-time
|
SESSION_TIME_TO_LIVE = None # In seconds. None - live forever, 0 - one-time
|
||||||
|
|
||||||
RETRY_ON_ERROR = False
|
RETRY_ON_ERROR = False
|
||||||
@ -45,6 +48,7 @@ def _get_req_session(reset=False):
|
|||||||
if SESSION_TIME_TO_LIVE:
|
if SESSION_TIME_TO_LIVE:
|
||||||
# If session TTL is set - check time passed
|
# If session TTL is set - check time passed
|
||||||
creation_date = util.per_thread('req_session_time', lambda: datetime.now(), reset)
|
creation_date = util.per_thread('req_session_time', lambda: datetime.now(), reset)
|
||||||
|
# noinspection PyTypeChecker
|
||||||
if (datetime.now() - creation_date).total_seconds() > SESSION_TIME_TO_LIVE:
|
if (datetime.now() - creation_date).total_seconds() > SESSION_TIME_TO_LIVE:
|
||||||
# Force session reset
|
# Force session reset
|
||||||
reset = True
|
reset = True
|
||||||
@ -72,6 +76,7 @@ def _make_request(token, method_name, method='get', params=None, files=None):
|
|||||||
if not token:
|
if not token:
|
||||||
raise Exception('Bot token is not defined')
|
raise Exception('Bot token is not defined')
|
||||||
if API_URL:
|
if API_URL:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
request_url = API_URL.format(token, method_name)
|
request_url = API_URL.format(token, method_name)
|
||||||
else:
|
else:
|
||||||
request_url = "https://api.telegram.org/bot{0}/{1}".format(token, method_name)
|
request_url = "https://api.telegram.org/bot{0}/{1}".format(token, method_name)
|
||||||
@ -83,17 +88,21 @@ def _make_request(token, method_name, method='get', params=None, files=None):
|
|||||||
fields.format_header_param = _no_encode(format_header_param)
|
fields.format_header_param = _no_encode(format_header_param)
|
||||||
if params:
|
if params:
|
||||||
if 'timeout' in params:
|
if 'timeout' in params:
|
||||||
read_timeout = params.pop('timeout') + 10
|
read_timeout = params.pop('timeout')
|
||||||
if 'connect-timeout' in params:
|
connect_timeout = read_timeout
|
||||||
connect_timeout = params.pop('connect-timeout') + 10
|
# if 'connect-timeout' in params:
|
||||||
|
# connect_timeout = params.pop('connect-timeout') + 10
|
||||||
if 'long_polling_timeout' in params:
|
if 'long_polling_timeout' in params:
|
||||||
# For getUpdates
|
# For getUpdates: it's the only function with timeout parameter on the BOT API side
|
||||||
# The only function with timeout on the BOT API side
|
long_polling_timeout = params.pop('long_polling_timeout')
|
||||||
params['timeout'] = params.pop('long_polling_timeout')
|
params['timeout'] = long_polling_timeout
|
||||||
# Long polling hangs for given time. Read timeout should be greater that long_polling_timeout
|
# Long polling hangs for a given time. Read timeout should be greater that long_polling_timeout
|
||||||
read_timeout = max(params['timeout'] + 10, read_timeout)
|
read_timeout = max(long_polling_timeout + 5, read_timeout)
|
||||||
|
# Lets stop suppose that user is stupid and assume that he knows what he do...
|
||||||
|
# read_timeout = read_timeout + 10
|
||||||
|
# connect_timeout = connect_timeout + 10
|
||||||
|
|
||||||
params = params or None #set params to None if empty
|
params = params or None # Set params to None if empty
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
if RETRY_ON_ERROR:
|
if RETRY_ON_ERROR:
|
||||||
@ -189,13 +198,15 @@ def get_file_url(token, file_id):
|
|||||||
if FILE_URL is None:
|
if FILE_URL is None:
|
||||||
return "https://api.telegram.org/file/bot{0}/{1}".format(token, get_file(token, file_id)['file_path'])
|
return "https://api.telegram.org/file/bot{0}/{1}".format(token, get_file(token, file_id)['file_path'])
|
||||||
else:
|
else:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
return FILE_URL.format(token, get_file(token, file_id)['file_path'])
|
return FILE_URL.format(token, get_file(token, file_id)['file_path'])
|
||||||
|
|
||||||
|
|
||||||
def download_file(token, file_path):
|
def download_file(token, file_path):
|
||||||
if FILE_URL is None:
|
if FILE_URL is None:
|
||||||
url = "https://api.telegram.org/file/bot{0}/{1}".format(token, file_path)
|
url = "https://api.telegram.org/file/bot{0}/{1}".format(token, file_path)
|
||||||
else:
|
else:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
url = FILE_URL.format(token, file_path)
|
url = FILE_URL.format(token, file_path)
|
||||||
|
|
||||||
result = _get_req_session().get(url, proxies=proxy)
|
result = _get_req_session().get(url, proxies=proxy)
|
||||||
@ -238,7 +249,7 @@ def send_message(
|
|||||||
if disable_notification is not None:
|
if disable_notification is not None:
|
||||||
payload['disable_notification'] = disable_notification
|
payload['disable_notification'] = disable_notification
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if entities:
|
if entities:
|
||||||
payload['entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(entities))
|
payload['entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(entities))
|
||||||
if allow_sending_without_reply is not None:
|
if allow_sending_without_reply is not None:
|
||||||
@ -264,7 +275,7 @@ def set_webhook(token, url=None, certificate=None, max_connections=None, allowed
|
|||||||
if drop_pending_updates is not None: # Any bool value should pass
|
if drop_pending_updates is not None: # Any bool value should pass
|
||||||
payload['drop_pending_updates'] = drop_pending_updates
|
payload['drop_pending_updates'] = drop_pending_updates
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
return _make_request(token, method_url, params=payload, files=files)
|
return _make_request(token, method_url, params=payload, files=files)
|
||||||
|
|
||||||
|
|
||||||
@ -274,7 +285,7 @@ def delete_webhook(token, drop_pending_updates=None, timeout=None):
|
|||||||
if drop_pending_updates is not None: # Any bool value should pass
|
if drop_pending_updates is not None: # Any bool value should pass
|
||||||
payload['drop_pending_updates'] = drop_pending_updates
|
payload['drop_pending_updates'] = drop_pending_updates
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
|
|
||||||
|
|
||||||
@ -282,7 +293,7 @@ def get_webhook_info(token, timeout=None):
|
|||||||
method_url = r'getWebhookInfo'
|
method_url = r'getWebhookInfo'
|
||||||
payload = {}
|
payload = {}
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
|
|
||||||
|
|
||||||
@ -294,9 +305,8 @@ def get_updates(token, offset=None, limit=None, timeout=None, allowed_updates=No
|
|||||||
if limit:
|
if limit:
|
||||||
payload['limit'] = limit
|
payload['limit'] = limit
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if long_polling_timeout:
|
payload['long_polling_timeout'] = long_polling_timeout if long_polling_timeout else LONG_POLLING_TIMEOUT
|
||||||
payload['long_polling_timeout'] = long_polling_timeout
|
|
||||||
if allowed_updates is not None: # Empty lists should pass
|
if allowed_updates is not None: # Empty lists should pass
|
||||||
payload['allowed_updates'] = json.dumps(allowed_updates)
|
payload['allowed_updates'] = json.dumps(allowed_updates)
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
@ -374,7 +384,7 @@ def forward_message(
|
|||||||
if disable_notification is not None:
|
if disable_notification is not None:
|
||||||
payload['disable_notification'] = disable_notification
|
payload['disable_notification'] = disable_notification
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
|
|
||||||
|
|
||||||
@ -398,7 +408,7 @@ def copy_message(token, chat_id, from_chat_id, message_id, caption=None, parse_m
|
|||||||
if allow_sending_without_reply is not None:
|
if allow_sending_without_reply is not None:
|
||||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
|
|
||||||
|
|
||||||
@ -417,7 +427,7 @@ def send_dice(
|
|||||||
if reply_markup:
|
if reply_markup:
|
||||||
payload['reply_markup'] = _convert_markup(reply_markup)
|
payload['reply_markup'] = _convert_markup(reply_markup)
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if allow_sending_without_reply is not None:
|
if allow_sending_without_reply is not None:
|
||||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
@ -448,7 +458,7 @@ def send_photo(
|
|||||||
if disable_notification is not None:
|
if disable_notification is not None:
|
||||||
payload['disable_notification'] = disable_notification
|
payload['disable_notification'] = disable_notification
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if caption_entities:
|
if caption_entities:
|
||||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||||
if allow_sending_without_reply is not None:
|
if allow_sending_without_reply is not None:
|
||||||
@ -468,7 +478,7 @@ def send_media_group(
|
|||||||
if reply_to_message_id:
|
if reply_to_message_id:
|
||||||
payload['reply_to_message_id'] = reply_to_message_id
|
payload['reply_to_message_id'] = reply_to_message_id
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if allow_sending_without_reply is not None:
|
if allow_sending_without_reply is not None:
|
||||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||||
return _make_request(
|
return _make_request(
|
||||||
@ -502,7 +512,7 @@ def send_location(
|
|||||||
if disable_notification is not None:
|
if disable_notification is not None:
|
||||||
payload['disable_notification'] = disable_notification
|
payload['disable_notification'] = disable_notification
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
|
|
||||||
|
|
||||||
@ -527,7 +537,7 @@ def edit_message_live_location(
|
|||||||
if reply_markup:
|
if reply_markup:
|
||||||
payload['reply_markup'] = _convert_markup(reply_markup)
|
payload['reply_markup'] = _convert_markup(reply_markup)
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
|
|
||||||
|
|
||||||
@ -545,7 +555,7 @@ def stop_message_live_location(
|
|||||||
if reply_markup:
|
if reply_markup:
|
||||||
payload['reply_markup'] = _convert_markup(reply_markup)
|
payload['reply_markup'] = _convert_markup(reply_markup)
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
|
|
||||||
|
|
||||||
@ -568,7 +578,7 @@ def send_venue(
|
|||||||
if reply_markup:
|
if reply_markup:
|
||||||
payload['reply_markup'] = _convert_markup(reply_markup)
|
payload['reply_markup'] = _convert_markup(reply_markup)
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if allow_sending_without_reply is not None:
|
if allow_sending_without_reply is not None:
|
||||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||||
if google_place_id:
|
if google_place_id:
|
||||||
@ -595,7 +605,7 @@ def send_contact(
|
|||||||
if reply_markup:
|
if reply_markup:
|
||||||
payload['reply_markup'] = _convert_markup(reply_markup)
|
payload['reply_markup'] = _convert_markup(reply_markup)
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if allow_sending_without_reply is not None:
|
if allow_sending_without_reply is not None:
|
||||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
@ -605,7 +615,7 @@ def send_chat_action(token, chat_id, action, timeout=None):
|
|||||||
method_url = r'sendChatAction'
|
method_url = r'sendChatAction'
|
||||||
payload = {'chat_id': chat_id, 'action': action}
|
payload = {'chat_id': chat_id, 'action': action}
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
|
|
||||||
|
|
||||||
@ -634,7 +644,7 @@ def send_video(token, chat_id, data, duration=None, caption=None, reply_to_messa
|
|||||||
if disable_notification is not None:
|
if disable_notification is not None:
|
||||||
payload['disable_notification'] = disable_notification
|
payload['disable_notification'] = disable_notification
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if thumb:
|
if thumb:
|
||||||
if not util.is_string(thumb):
|
if not util.is_string(thumb):
|
||||||
if files:
|
if files:
|
||||||
@ -678,7 +688,7 @@ def send_animation(
|
|||||||
if disable_notification is not None:
|
if disable_notification is not None:
|
||||||
payload['disable_notification'] = disable_notification
|
payload['disable_notification'] = disable_notification
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if thumb:
|
if thumb:
|
||||||
if not util.is_string(thumb):
|
if not util.is_string(thumb):
|
||||||
if files:
|
if files:
|
||||||
@ -717,7 +727,7 @@ def send_voice(token, chat_id, voice, caption=None, duration=None, reply_to_mess
|
|||||||
if disable_notification is not None:
|
if disable_notification is not None:
|
||||||
payload['disable_notification'] = disable_notification
|
payload['disable_notification'] = disable_notification
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if caption_entities:
|
if caption_entities:
|
||||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||||
if allow_sending_without_reply is not None:
|
if allow_sending_without_reply is not None:
|
||||||
@ -747,7 +757,7 @@ def send_video_note(token, chat_id, data, duration=None, length=None, reply_to_m
|
|||||||
if disable_notification is not None:
|
if disable_notification is not None:
|
||||||
payload['disable_notification'] = disable_notification
|
payload['disable_notification'] = disable_notification
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if thumb:
|
if thumb:
|
||||||
if not util.is_string(thumb):
|
if not util.is_string(thumb):
|
||||||
if files:
|
if files:
|
||||||
@ -788,7 +798,7 @@ def send_audio(token, chat_id, audio, caption=None, duration=None, performer=Non
|
|||||||
if disable_notification is not None:
|
if disable_notification is not None:
|
||||||
payload['disable_notification'] = disable_notification
|
payload['disable_notification'] = disable_notification
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if thumb:
|
if thumb:
|
||||||
if not util.is_string(thumb):
|
if not util.is_string(thumb):
|
||||||
if files:
|
if files:
|
||||||
@ -826,7 +836,7 @@ def send_data(token, chat_id, data, data_type, reply_to_message_id=None, reply_m
|
|||||||
if disable_notification is not None:
|
if disable_notification is not None:
|
||||||
payload['disable_notification'] = disable_notification
|
payload['disable_notification'] = disable_notification
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if caption:
|
if caption:
|
||||||
payload['caption'] = caption
|
payload['caption'] = caption
|
||||||
if thumb:
|
if thumb:
|
||||||
@ -1162,7 +1172,7 @@ def delete_message(token, chat_id, message_id, timeout=None):
|
|||||||
method_url = r'deleteMessage'
|
method_url = r'deleteMessage'
|
||||||
payload = {'chat_id': chat_id, 'message_id': message_id}
|
payload = {'chat_id': chat_id, 'message_id': message_id}
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
return _make_request(token, method_url, params=payload, method='post')
|
return _make_request(token, method_url, params=payload, method='post')
|
||||||
|
|
||||||
|
|
||||||
@ -1181,7 +1191,7 @@ def send_game(
|
|||||||
if reply_markup:
|
if reply_markup:
|
||||||
payload['reply_markup'] = _convert_markup(reply_markup)
|
payload['reply_markup'] = _convert_markup(reply_markup)
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if allow_sending_without_reply is not None:
|
if allow_sending_without_reply is not None:
|
||||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
@ -1314,7 +1324,7 @@ def send_invoice(
|
|||||||
if provider_data:
|
if provider_data:
|
||||||
payload['provider_data'] = provider_data
|
payload['provider_data'] = provider_data
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if allow_sending_without_reply is not None:
|
if allow_sending_without_reply is not None:
|
||||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||||
return _make_request(token, method_url, params=payload)
|
return _make_request(token, method_url, params=payload)
|
||||||
@ -1458,6 +1468,7 @@ def delete_sticker_from_set(token, sticker):
|
|||||||
return _make_request(token, method_url, params=payload, method='post')
|
return _make_request(token, method_url, params=payload, method='post')
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyShadowingBuiltins
|
||||||
def send_poll(
|
def send_poll(
|
||||||
token, chat_id,
|
token, chat_id,
|
||||||
question, options,
|
question, options,
|
||||||
@ -1502,7 +1513,7 @@ def send_poll(
|
|||||||
if reply_markup is not None:
|
if reply_markup is not None:
|
||||||
payload['reply_markup'] = _convert_markup(reply_markup)
|
payload['reply_markup'] = _convert_markup(reply_markup)
|
||||||
if timeout:
|
if timeout:
|
||||||
payload['connect-timeout'] = timeout
|
payload['timeout'] = timeout
|
||||||
if explanation_entities:
|
if explanation_entities:
|
||||||
payload['explanation_entities'] = json.dumps(
|
payload['explanation_entities'] = json.dumps(
|
||||||
types.MessageEntity.to_list_of_dicts(explanation_entities))
|
types.MessageEntity.to_list_of_dicts(explanation_entities))
|
||||||
|
Loading…
Reference in New Issue
Block a user