diff --git a/.github/workflows/setup_python.yml b/.github/workflows/setup_python.yml
index 5518ff2f3..670251511 100644
--- a/.github/workflows/setup_python.yml
+++ b/.github/workflows/setup_python.yml
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', 'pypy-3.8', 'pypy-3.9', 'pypy-3.10']
+ python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', 'pypy-3.9', 'pypy-3.10']
name: ${{ matrix.python-version }} and tests
steps:
- uses: actions/checkout@v2
diff --git a/README.md b/README.md
index 10694e4b0..22be0a24c 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
A simple, but extensible Python implementation for the Telegram Bot API.
Both synchronous and asynchronous.
-## Supported Bot API version: 7.5!
+##
Supported Bot API version: 7.6!
diff --git a/docs/source/conf.py b/docs/source/conf.py
index f2b1deb6b..cfa926801 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -22,7 +22,7 @@
copyright = f'2022-{datetime.now().year}, {author}'
# The full version, including alpha/beta/rc tags
-release = '4.20.0'
+release = '4.21.0'
# -- General configuration ---------------------------------------------------
diff --git a/pyproject.toml b/pyproject.toml
index 75b695f44..0095c3f21 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "pyTelegramBotAPI"
-version = "4.20.0"
+version = "4.21.0"
description = "Python Telegram bot api."
authors = [{name = "eternnoir", email = "eternnoir@gmail.com"}]
license = {text = "GPL2"}
diff --git a/telebot/__init__.py b/telebot/__init__.py
index 8fe414252..31370d3e3 100644
--- a/telebot/__init__.py
+++ b/telebot/__init__.py
@@ -1830,6 +1830,9 @@ def copy_message(
show_caption_above_media: Optional[bool]=None) -> types.MessageID:
"""
Use this method to copy messages of any kind.
+ Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied.
+ A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous to the method
+ forwardMessage, but the copied message doesn't have a link to the original message. Returns the MessageId of the sent message on success.
Telegram documentation: https://core.telegram.org/bots/api#copymessage
@@ -2007,47 +2010,47 @@ def forward_messages(self, chat_id: Union[str, int], from_chat_id: Union[str, in
def copy_messages(self, chat_id: Union[str, int], from_chat_id: Union[str, int], message_ids: List[int],
disable_notification: Optional[bool] = None, message_thread_id: Optional[int] = None,
protect_content: Optional[bool] = None, remove_caption: Optional[bool] = None) -> List[types.MessageID]:
- """
- Use this method to copy messages of any kind. If some of the specified messages can't be found or copied, they are skipped.
- Service messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied.
- A quiz poll can be copied only if the value of the field correct_option_id is known to the bot.
- The method is analogous to the method forwardMessages, but the copied messages don't have a link to the original message.
- Album grouping is kept for copied messages. On success, an array of MessageId of the sent messages is returned.
+ """
+ Use this method to copy messages of any kind.
+ If some of the specified messages can't be found or copied, they are skipped. Service messages, paid media messages, giveaway messages, giveaway winners messages,
+ and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous
+ to the method forwardMessages, but the copied messages don't have a link to the original message. Album grouping is kept for copied messages. On success, an array
+ of MessageId of the sent messages is returned.
- Telegram documentation: https://core.telegram.org/bots/api#copymessages
+ Telegram documentation: https://core.telegram.org/bots/api#copymessages
- :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername)
- :type chat_id: :obj:`int` or :obj:`str`
+ :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername)
+ :type chat_id: :obj:`int` or :obj:`str`
- :param from_chat_id: Unique identifier for the chat where the original message was sent (or channel username in the format @channelusername)
- :type from_chat_id: :obj:`int` or :obj:`str`
+ :param from_chat_id: Unique identifier for the chat where the original message was sent (or channel username in the format @channelusername)
+ :type from_chat_id: :obj:`int` or :obj:`str`
- :param message_ids: Message identifiers in the chat specified in from_chat_id
- :type message_ids: :obj:`list` of :obj:`int`
+ :param message_ids: Message identifiers in the chat specified in from_chat_id
+ :type message_ids: :obj:`list` of :obj:`int`
- :param disable_notification: Sends the message silently. Users will receive a notification with no sound
- :type disable_notification: :obj:`bool`
+ :param disable_notification: Sends the message silently. Users will receive a notification with no sound
+ :type disable_notification: :obj:`bool`
- :param message_thread_id: Identifier of a message thread, in which the messages will be sent
- :type message_thread_id: :obj:`int`
+ :param message_thread_id: Identifier of a message thread, in which the messages will be sent
+ :type message_thread_id: :obj:`int`
- :param protect_content: Protects the contents of the forwarded message from forwarding and saving
- :type protect_content: :obj:`bool`
+ :param protect_content: Protects the contents of the forwarded message from forwarding and saving
+ :type protect_content: :obj:`bool`
- :param remove_caption: Pass True to copy the messages without their captions
- :type remove_caption: :obj:`bool`
+ :param remove_caption: Pass True to copy the messages without their captions
+ :type remove_caption: :obj:`bool`
- :return: On success, an array of MessageId of the sent messages is returned.
- :rtype: :obj:`list` of :class:`telebot.types.MessageID`
- """
- disable_notification = self.disable_notification if disable_notification is None else disable_notification
- protect_content = self.protect_content if protect_content is None else protect_content
+ :return: On success, an array of MessageId of the sent messages is returned.
+ :rtype: :obj:`list` of :class:`telebot.types.MessageID`
+ """
+ disable_notification = self.disable_notification if disable_notification is None else disable_notification
+ protect_content = self.protect_content if protect_content is None else protect_content
- result = apihelper.copy_messages(
- self.token, chat_id, from_chat_id, message_ids, disable_notification=disable_notification,
- message_thread_id=message_thread_id, protect_content=protect_content, remove_caption=remove_caption)
- return [types.MessageID.de_json(message_id) for message_id in result]
+ result = apihelper.copy_messages(
+ self.token, chat_id, from_chat_id, message_ids, disable_notification=disable_notification,
+ message_thread_id=message_thread_id, protect_content=protect_content, remove_caption=remove_caption)
+ return [types.MessageID.de_json(message_id) for message_id in result]
def send_dice(
@@ -2606,6 +2609,10 @@ def send_document(
logger.warning('The parameter "thumb" is deprecated. Use "thumbnail" instead.')
thumbnail = thumb
+ if isinstance(document, types.InputFile) and visible_file_name:
+ # inputfile name ignored, warn
+ logger.warning('Cannot use both InputFile and visible_file_name. InputFile name will be ignored.')
+
return types.Message.de_json(
apihelper.send_data(
self.token, chat_id, document, 'document',
@@ -3120,6 +3127,61 @@ def send_video_note(
protect_content=protect_content, message_thread_id=message_thread_id, reply_parameters=reply_parameters,
business_connection_id=business_connection_id, message_effect_id=message_effect_id)
)
+
+ def send_paid_media(
+ self, chat_id: Union[int, str], star_count: int, media: List[types.InputPaidMedia],
+ caption: Optional[str]=None, parse_mode: Optional[str]=None, caption_entities: Optional[List[types.MessageEntity]]=None,
+ show_caption_above_media: Optional[bool]=None, disable_notification: Optional[bool]=None,
+ protect_content: Optional[bool]=None, reply_parameters: Optional[types.ReplyParameters]=None,
+ reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> types.Message:
+ """
+ Use this method to send paid media to channel chats. On success, the sent Message is returned.
+
+ Telegram documentation: https://core.telegram.org/bots/api#sendpaidmedia
+
+ :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername)
+ :type chat_id: :obj:`int` or :obj:`str`
+
+ :param star_count: The number of Telegram Stars that must be paid to buy access to the media
+ :type star_count: :obj:`int`
+
+ :param media: A JSON-serialized array describing the media to be sent; up to 10 items
+ :type media: :obj:`list` of :class:`telebot.types.InputPaidMedia`
+
+ :param caption: Media caption, 0-1024 characters after entities parsing
+ :type caption: :obj:`str`
+
+ :param parse_mode: Mode for parsing entities in the media caption
+ :type parse_mode: :obj:`str`
+
+ :param caption_entities: List of special entities that appear in the caption, which can be specified instead of parse_mode
+ :type caption_entities: :obj:`list` of :class:`telebot.types.MessageEntity`
+
+ :param show_caption_above_media: Pass True, if the caption must be shown above the message media
+ :type show_caption_above_media: :obj:`bool`
+
+ :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
+ :type disable_notification: :obj:`bool`
+
+ :param protect_content: Protects the contents of the sent message from forwarding and saving
+ :type protect_content: :obj:`bool`
+
+ :param reply_parameters: Description of the message to reply to
+ :type reply_parameters: :class:`telebot.types.ReplyParameters`
+
+ :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
+ :type reply_markup: :class:`telebot.types.InlineKeyboardMarkup` or :class:`telebot.types.ReplyKeyboardMarkup` or :class:`telebot.types.ReplyKeyboardRemove` or :class:`telebot.types.ForceReply`
+
+ :return: On success, the sent Message is returned.
+ :rtype: :class:`telebot.types.Message`
+ """
+ return types.Message.de_json(
+ apihelper.send_paid_media(
+ self.token, chat_id, star_count, media, caption=caption, parse_mode=parse_mode,
+ caption_entities=caption_entities, show_caption_above_media=show_caption_above_media,
+ disable_notification=disable_notification, protect_content=protect_content,
+ reply_parameters=reply_parameters, reply_markup=reply_markup)
+ )
def send_media_group(
@@ -4731,7 +4793,7 @@ def edit_message_text(
parse_mode=parse_mode, entities=entities, reply_markup=reply_markup, link_preview_options=link_preview_options,
business_connection_id=business_connection_id, timeout=timeout)
- if type(result) == bool: # if edit inline message return is bool not Message.
+ if isinstance(result, bool): # if edit inline message return is bool not Message.
return result
return types.Message.de_json(result)
@@ -4778,7 +4840,7 @@ def edit_message_media(
self.token, media, chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id,
reply_markup=reply_markup, business_connection_id=business_connection_id, timeout=timeout)
- if type(result) == bool: # if edit inline message return is bool not Message.
+ if isinstance(result, bool): # if edit inline message return is bool not Message.
return result
return types.Message.de_json(result)
@@ -4820,7 +4882,7 @@ def edit_message_reply_markup(
self.token, chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id,
reply_markup=reply_markup, business_connection_id=business_connection_id, timeout=timeout)
- if type(result) == bool:
+ if isinstance(result, bool):
return result
return types.Message.de_json(result)
@@ -4954,7 +5016,7 @@ def set_game_score(
self.token, user_id, score, force=force, disable_edit_message=disable_edit_message,
chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id)
- if type(result) == bool:
+ if isinstance(result, bool):
return result
return types.Message.de_json(result)
@@ -5618,7 +5680,7 @@ def edit_message_caption(
show_caption_above_media=show_caption_above_media, business_connection_id=business_connection_id,
timeout=timeout)
- if type(result) == bool:
+ if isinstance(result, bool):
return result
return types.Message.de_json(result)
diff --git a/telebot/apihelper.py b/telebot/apihelper.py
index 54ce9f691..0bcb80a96 100644
--- a/telebot/apihelper.py
+++ b/telebot/apihelper.py
@@ -94,7 +94,7 @@ def _make_request(token, method_name, method='get', params=None, files=None):
# process types.InputFile
for key, value in files_copy.items():
if isinstance(value, types.InputFile):
- files[key] = value.file
+ files[key] = (value.file_name, value.file)
elif isinstance(value, tuple) and (len(value) == 2) and isinstance(value[1], types.InputFile):
files[key] = (value[0], value[1].file)
@@ -525,6 +525,34 @@ def send_photo(
if show_caption_above_media is not None:
payload['show_caption_above_media'] = show_caption_above_media
return _make_request(token, method_url, params=payload, files=files, method='post')
+
+def send_paid_media(
+ token, chat_id, star_count, media,
+ caption=None, parse_mode=None, caption_entities=None, show_caption_above_media=None,
+ disable_notification=None, protect_content=None, reply_parameters=None, reply_markup=None):
+ method_url = r'sendPaidMedia'
+ media_json, files = convert_input_media_array(media)
+ payload = {'chat_id': chat_id, 'star_count': star_count, 'media': media_json}
+ if caption:
+ payload['caption'] = caption
+ if parse_mode:
+ payload['parse_mode'] = parse_mode
+ if caption_entities:
+ payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
+ if show_caption_above_media is not None:
+ payload['show_caption_above_media'] = show_caption_above_media
+ if disable_notification is not None:
+ payload['disable_notification'] = disable_notification
+ if protect_content is not None:
+ payload['protect_content'] = protect_content
+ if reply_parameters is not None:
+ payload['reply_parameters'] = reply_parameters.to_json()
+ if reply_markup:
+ payload['reply_markup'] = _convert_markup(reply_markup)
+ return _make_request(
+ token, method_url, params=payload,
+ method='post' if files else 'get',
+ files=files if files else None)
def send_media_group(
@@ -2117,7 +2145,7 @@ def convert_input_media_array(array):
media = []
files = {}
for input_media in array:
- if isinstance(input_media, types.InputMedia):
+ if isinstance(input_media, types.InputMedia) or isinstance(input_media, types.InputPaidMedia):
media_dict = input_media.to_dict()
if media_dict['media'].startswith('attach://'):
key = media_dict['media'].replace('attach://', '')
diff --git a/telebot/async_telebot.py b/telebot/async_telebot.py
index 224856416..d8dd91e05 100644
--- a/telebot/async_telebot.py
+++ b/telebot/async_telebot.py
@@ -246,7 +246,7 @@ def _setup_change_detector(self, path_to_watch: str) -> None:
self.event_observer.schedule(self.event_handler, path, recursive=True)
self.event_observer.start()
- async def polling(self, non_stop: bool=False, skip_pending=False, interval: int=0, timeout: int=20,
+ async def polling(self, non_stop: bool=True, skip_pending=False, interval: int=0, timeout: int=20,
request_timeout: Optional[int]=None, allowed_updates: Optional[List[str]]=None,
none_stop: Optional[bool]=None, restart_on_change: Optional[bool]=False, path_to_watch: Optional[str]=None):
"""
@@ -257,11 +257,6 @@ async def polling(self, non_stop: bool=False, skip_pending=False, interval: int=
Always gets updates.
- .. note::
-
- Set non_stop=True if you want your bot to continue receiving updates
- if there is an error.
-
.. note::
Install watchdog and psutil before using restart_on_change option.
@@ -393,6 +388,15 @@ def __hide_token(self, message: str) -> str:
return message.replace(code, "*" * len(code))
else:
return message
+
+ async def _handle_error_interval(self, error_interval: float):
+ logger.debug('Waiting for %s seconds before retrying', error_interval)
+ await asyncio.sleep(error_interval)
+ if error_interval * 2 < 60: # same logic as sync
+ error_interval *= 2
+ else:
+ error_interval = 60
+ return error_interval
async def _process_polling(self, non_stop: bool=False, interval: int=0, timeout: int=20,
request_timeout: int=None, allowed_updates: Optional[List[str]]=None):
@@ -426,16 +430,18 @@ async def _process_polling(self, non_stop: bool=False, interval: int=0, timeout:
self._polling = True
+ error_interval = 0.25
+
try:
while self._polling:
try:
-
updates = await self.get_updates(offset=self.offset, allowed_updates=allowed_updates, timeout=timeout, request_timeout=request_timeout)
if updates:
self.offset = updates[-1].update_id + 1
# noinspection PyAsyncCall
asyncio.create_task(self.process_new_updates(updates)) # Seperate task for processing updates
if interval: await asyncio.sleep(interval)
+ error_interval = 0.25 # drop error_interval if no errors
except KeyboardInterrupt:
return
@@ -446,9 +452,11 @@ async def _process_polling(self, non_stop: bool=False, interval: int=0, timeout:
if not handled:
logger.error('Unhandled exception (full traceback for debug level): %s', self.__hide_token(str(e)))
logger.debug(self.__hide_token(traceback.format_exc()))
+
+ if non_stop:
+ error_interval = await self._handle_error_interval(error_interval)
if non_stop or handled:
- await asyncio.sleep(2)
continue
else:
return
@@ -458,6 +466,9 @@ async def _process_polling(self, non_stop: bool=False, interval: int=0, timeout:
logger.error('Unhandled exception (full traceback for debug level): %s', self.__hide_token(str(e)))
logger.debug(self.__hide_token(traceback.format_exc()))
+ if non_stop:
+ error_interval = await self._handle_error_interval(error_interval)
+
if non_stop or handled:
continue
else:
@@ -468,6 +479,9 @@ async def _process_polling(self, non_stop: bool=False, interval: int=0, timeout:
logger.error('Unhandled exception (full traceback for debug level): %s', str(e))
logger.debug(traceback.format_exc())
+ if non_stop:
+ error_interval = await self._handle_error_interval(error_interval)
+
if non_stop or handled:
continue
else:
@@ -3248,6 +3262,10 @@ async def copy_message(
show_caption_above_media: Optional[bool]=None) -> types.MessageID:
"""
Use this method to copy messages of any kind.
+ If some of the specified messages can't be found or copied, they are skipped. Service messages, paid media messages, giveaway messages, giveaway winners messages,
+ and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous
+ to the method forwardMessages, but the copied messages don't have a link to the original message. Album grouping is kept for copied messages. On success, an array
+ of MessageId of the sent messages is returned.
Telegram documentation: https://core.telegram.org/bots/api#copymessage
@@ -3415,44 +3433,43 @@ async def forward_messages(self, chat_id: Union[str, int], from_chat_id: Union[s
async def copy_messages(self, chat_id: Union[str, int], from_chat_id: Union[str, int], message_ids: List[int],
disable_notification: Optional[bool] = None, message_thread_id: Optional[int] = None,
protect_content: Optional[bool] = None, remove_caption: Optional[bool] = None) -> List[types.MessageID]:
- """
- Use this method to copy messages of any kind. If some of the specified messages can't be found or copied, they are skipped.
- Service messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied
- only if the value of the field correct_option_id is known to the bot. The method is analogous to the method forwardMessages, but
- the copied messages don't have a link to the original message. Album grouping is kept for copied messages.
- On success, an array of MessageId of the sent messages is returned.
+ """
+ Use this method to copy messages of any kind.
+ Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied.
+ A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous to the method
+ forwardMessage, but the copied message doesn't have a link to the original message. Returns the MessageId of the sent message on success.
- Telegram documentation: https://core.telegram.org/bots/api#copymessages
+ Telegram documentation: https://core.telegram.org/bots/api#copymessages
- :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername)
- :type chat_id: :obj:`int` or :obj:`str`
+ :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername)
+ :type chat_id: :obj:`int` or :obj:`str`
- :param from_chat_id: Unique identifier for the chat where the original message was sent (or channel username in the format @channelusername)
- :type from_chat_id: :obj:`int` or :obj:`str`
+ :param from_chat_id: Unique identifier for the chat where the original message was sent (or channel username in the format @channelusername)
+ :type from_chat_id: :obj:`int` or :obj:`str`
- :param message_ids: Message identifiers in the chat specified in from_chat_id
- :type message_ids: :obj:`list` of :obj:`int`
+ :param message_ids: Message identifiers in the chat specified in from_chat_id
+ :type message_ids: :obj:`list` of :obj:`int`
- :param disable_notification: Sends the message silently. Users will receive a notification with no sound
- :type disable_notification: :obj:`bool`
+ :param disable_notification: Sends the message silently. Users will receive a notification with no sound
+ :type disable_notification: :obj:`bool`
- :param message_thread_id: Identifier of a message thread, in which the messages will be sent
- :type message_thread_id: :obj:`int`
+ :param message_thread_id: Identifier of a message thread, in which the messages will be sent
+ :type message_thread_id: :obj:`int`
- :param protect_content: Protects the contents of the forwarded message from forwarding and saving
- :type protect_content: :obj:`bool`
+ :param protect_content: Protects the contents of the forwarded message from forwarding and saving
+ :type protect_content: :obj:`bool`
- :param remove_caption: Pass True to copy the messages without their captions
- :type remove_caption: :obj:`bool`
+ :param remove_caption: Pass True to copy the messages without their captions
+ :type remove_caption: :obj:`bool`
- :return: On success, an array of MessageId of the sent messages is returned.
- :rtype: :obj:`list` of :class:`telebot.types.MessageID`
- """
- disable_notification = self.disable_notification if disable_notification is None else disable_notification
- protect_content = self.protect_content if protect_content is None else protect_content
- result = await asyncio_helper.copy_messages(self.token, chat_id, from_chat_id, message_ids, disable_notification, message_thread_id,
- protect_content, remove_caption)
- return [types.MessageID.de_json(message_id) for message_id in result]
+ :return: On success, an array of MessageId of the sent messages is returned.
+ :rtype: :obj:`list` of :class:`telebot.types.MessageID`
+ """
+ disable_notification = self.disable_notification if disable_notification is None else disable_notification
+ protect_content = self.protect_content if protect_content is None else protect_content
+ result = await asyncio_helper.copy_messages(self.token, chat_id, from_chat_id, message_ids, disable_notification, message_thread_id,
+ protect_content, remove_caption)
+ return [types.MessageID.de_json(message_id) for message_id in result]
async def send_dice(
self, chat_id: Union[int, str],
@@ -4014,6 +4031,10 @@ async def send_document(
if reply_parameters and (reply_parameters.allow_sending_without_reply is None):
reply_parameters.allow_sending_without_reply = self.allow_sending_without_reply
+ if isinstance(document, types.InputFile) and visible_file_name:
+ # inputfile name ignored, warn
+ logger.warning('Cannot use both InputFile and visible_file_name. InputFile name will be ignored.')
+
return types.Message.de_json(
await asyncio_helper.send_data(
self.token, chat_id, document, 'document',
@@ -4526,6 +4547,61 @@ async def send_video_note(
self.token, chat_id, data, duration, length, reply_markup,
disable_notification, timeout, thumbnail, protect_content, message_thread_id, reply_parameters, business_connection_id, message_effect_id=message_effect_id))
+ async def send_paid_media(
+ self, chat_id: Union[int, str], star_count: int, media: List[types.InputPaidMedia],
+ caption: Optional[str]=None, parse_mode: Optional[str]=None, caption_entities: Optional[List[types.MessageEntity]]=None,
+ show_caption_above_media: Optional[bool]=None, disable_notification: Optional[bool]=None,
+ protect_content: Optional[bool]=None, reply_parameters: Optional[types.ReplyParameters]=None,
+ reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> types.Message:
+ """
+ Use this method to send paid media to channel chats. On success, the sent Message is returned.
+
+ Telegram documentation: https://core.telegram.org/bots/api#sendpaidmedia
+
+ :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername)
+ :type chat_id: :obj:`int` or :obj:`str`
+
+ :param star_count: The number of Telegram Stars that must be paid to buy access to the media
+ :type star_count: :obj:`int`
+
+ :param media: A JSON-serialized array describing the media to be sent; up to 10 items
+ :type media: :obj:`list` of :class:`telebot.types.InputPaidMedia`
+
+ :param caption: Media caption, 0-1024 characters after entities parsing
+ :type caption: :obj:`str`
+
+ :param parse_mode: Mode for parsing entities in the media caption
+ :type parse_mode: :obj:`str`
+
+ :param caption_entities: List of special entities that appear in the caption, which can be specified instead of parse_mode
+ :type caption_entities: :obj:`list` of :class:`telebot.types.MessageEntity`
+
+ :param show_caption_above_media: Pass True, if the caption must be shown above the message media
+ :type show_caption_above_media: :obj:`bool`
+
+ :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
+ :type disable_notification: :obj:`bool`
+
+ :param protect_content: Protects the contents of the sent message from forwarding and saving
+ :type protect_content: :obj:`bool`
+
+ :param reply_parameters: Description of the message to reply to
+ :type reply_parameters: :class:`telebot.types.ReplyParameters`
+
+ :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
+ :type reply_markup: :class:`telebot.types.InlineKeyboardMarkup` or :class:`telebot.types.ReplyKeyboardMarkup` or :class:`telebot.types.ReplyKeyboardRemove` or :class:`telebot.types.ForceReply`
+
+ :return: On success, the sent Message is returned.
+ :rtype: :class:`telebot.types.Message`
+ """
+ return types.Message.de_json(
+ await asyncio_helper.send_paid_media(
+ self.token, chat_id, star_count, media, caption=caption, parse_mode=parse_mode,
+ caption_entities=caption_entities, show_caption_above_media=show_caption_above_media,
+ disable_notification=disable_notification, protect_content=protect_content,
+ reply_parameters=reply_parameters, reply_markup=reply_markup)
+ )
+
async def send_media_group(
self, chat_id: Union[int, str],
media: List[Union[
@@ -6087,7 +6163,7 @@ async def edit_message_text(
result = await asyncio_helper.edit_message_text(
self.token, text, chat_id, message_id, inline_message_id, parse_mode, entities, reply_markup,
link_preview_options, business_connection_id, timeout)
- if type(result) == bool: # if edit inline message return is bool not Message.
+ if isinstance(result, bool): # if edit inline message return is bool not Message.
return result
return types.Message.de_json(result)
@@ -6131,7 +6207,7 @@ async def edit_message_media(
"""
result = await asyncio_helper.edit_message_media(
self.token, media, chat_id, message_id, inline_message_id, reply_markup, business_connection_id, timeout)
- if type(result) == bool: # if edit inline message return is bool not Message.
+ if isinstance(result, bool): # if edit inline message return is bool not Message.
return result
return types.Message.de_json(result)
@@ -6170,7 +6246,7 @@ async def edit_message_reply_markup(
"""
result = await asyncio_helper.edit_message_reply_markup(
self.token, chat_id, message_id, inline_message_id, reply_markup, business_connection_id, timeout)
- if type(result) == bool:
+ if isinstance(result, bool):
return result
return types.Message.de_json(result)
@@ -6298,7 +6374,7 @@ async def set_game_score(
"""
result = await asyncio_helper.set_game_score(self.token, user_id, score, force, disable_edit_message, chat_id,
message_id, inline_message_id)
- if type(result) == bool:
+ if isinstance(result, bool):
return result
return types.Message.de_json(result)
@@ -6942,7 +7018,7 @@ async def edit_message_caption(
self.token, caption, chat_id, message_id, inline_message_id, parse_mode, caption_entities, reply_markup,
show_caption_above_media=show_caption_above_media, business_connection_id=business_connection_id,
timeout=timeout)
- if type(result) == bool:
+ if isinstance(result, bool):
return result
return types.Message.de_json(result)
diff --git a/telebot/asyncio_helper.py b/telebot/asyncio_helper.py
index 0a484f3b4..b70508a2a 100644
--- a/telebot/asyncio_helper.py
+++ b/telebot/asyncio_helper.py
@@ -130,6 +130,8 @@ def _prepare_data(params=None, files=None):
if isinstance(f, tuple):
if len(f) == 2:
file_name, file = f
+ if isinstance(file, types.InputFile):
+ file = file.file
else:
raise ValueError('Tuple must have exactly 2 elements: filename, fileobj')
elif isinstance(f, types.InputFile):
@@ -514,6 +516,33 @@ async def send_photo(
payload['show_caption_above_media'] = show_caption_above_media
return await _process_request(token, method_url, params=payload, files=files, method='post')
+async def send_paid_media(
+ token, chat_id, star_count, media,
+ caption=None, parse_mode=None, caption_entities=None, show_caption_above_media=None,
+ disable_notification=None, protect_content=None, reply_parameters=None, reply_markup=None):
+ method_url = r'sendPaidMedia'
+ media_json, files = convert_input_media_array(media)
+ payload = {'chat_id': chat_id, 'star_count': star_count, 'media': media_json}
+ if caption:
+ payload['caption'] = caption
+ if parse_mode:
+ payload['parse_mode'] = parse_mode
+ if caption_entities:
+ payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
+ if show_caption_above_media is not None:
+ payload['show_caption_above_media'] = show_caption_above_media
+ if disable_notification is not None:
+ payload['disable_notification'] = disable_notification
+ if protect_content is not None:
+ payload['protect_content'] = protect_content
+ if reply_parameters is not None:
+ payload['reply_parameters'] = reply_parameters.to_json()
+ if reply_markup:
+ payload['reply_markup'] = _convert_markup(reply_markup)
+ return await _process_request(
+ token, method_url, params=payload,
+ method='post' if files else 'get',
+ files=files if files else None)
async def send_media_group(
token, chat_id, media,
@@ -2081,7 +2110,7 @@ async def convert_input_media_array(array):
media = []
files = {}
for input_media in array:
- if isinstance(input_media, types.InputMedia):
+ if isinstance(input_media, types.InputMedia) or isinstance(input_media, types.InputPaidMedia):
media_dict = input_media.to_dict()
if media_dict['media'].startswith('attach://'):
key = media_dict['media'].replace('attach://', '')
diff --git a/telebot/custom_filters.py b/telebot/custom_filters.py
index 1c5bd0b40..ca91dd9b8 100644
--- a/telebot/custom_filters.py
+++ b/telebot/custom_filters.py
@@ -216,7 +216,7 @@ def check(self, message, text):
"""
if isinstance(text, TextFilter):
return text.check(message)
- elif type(text) is list:
+ elif isinstance(text, list):
return message.text in text
else:
return text == message.text
@@ -354,7 +354,7 @@ def check(self, message, text):
"""
:meta private:
"""
- if type(text) is list:
+ if isinstance(text, list):
return message.from_user.language_code in text
else:
return message.from_user.language_code == text
@@ -430,7 +430,6 @@ def check(self, message, text):
elif type(text) is list and user_state in text:
return True
-
class IsDigitFilter(SimpleCustomFilter):
"""
Filter to check whether the string is made up of only digits.
diff --git a/telebot/types.py b/telebot/types.py
index 5d69af0ff..d0d3819bb 100644
--- a/telebot/types.py
+++ b/telebot/types.py
@@ -707,6 +707,10 @@ class ChatFullInfo(JsonDeserializable):
:param location: Optional. For supergroups, the location to which the supergroup is connected. Returned only in getChat.
:type location: :class:`telebot.types.ChatLocation`
+ :param can_send_paid_media: Optional. True, if paid media messages can be sent or forwarded to the channel chat.
+ The field is available only for channel chats.
+ :type can_send_paid_media: :obj:`bool`
+
:return: Instance of the class
:rtype: :class:`telebot.types.ChatFullInfo`
"""
@@ -748,7 +752,8 @@ def __init__(self, id, type, title=None, username=None, first_name=None,
available_reactions=None, accent_color_id=None, background_custom_emoji_id=None, profile_accent_color_id=None,
profile_background_custom_emoji_id=None, has_visible_history=None,
unrestrict_boost_count=None, custom_emoji_sticker_set_name=None, business_intro=None, business_location=None,
- business_opening_hours=None, personal_chat=None, birthdate=None, **kwargs):
+ business_opening_hours=None, personal_chat=None, birthdate=None,
+ can_send_paid_media=None, **kwargs):
self.id: int = id
self.type: str = type
self.title: str = title
@@ -792,6 +797,7 @@ def __init__(self, id, type, title=None, username=None, first_name=None,
self.business_opening_hours: BusinessOpeningHours = business_opening_hours
self.personal_chat: Chat = personal_chat
self.birthdate: Birthdate = birthdate
+ self.can_send_paid_media: bool = can_send_paid_media
class Chat(ChatFullInfo):
@@ -964,6 +970,9 @@ class Message(JsonDeserializable):
:param document: Optional. Message is a general file, information about the file
:type document: :class:`telebot.types.Document`
+ :param paid_media: Optional. Message contains paid media; information about the paid media
+ :type paid_media: :class:`telebot.types.PaidMediaInfo`
+
:param photo: Optional. Message is a photo, available sizes of the photo
:type photo: :obj:`list` of :class:`telebot.types.PhotoSize`
@@ -1380,7 +1389,8 @@ def de_json(cls, json_string):
opts['effect_id'] = obj['effect_id']
if 'show_caption_above_media' in obj:
opts['show_caption_above_media'] = obj['show_caption_above_media']
-
+ if 'paid_media' in obj:
+ opts['paid_media'] = PaidMediaInfo.de_json(obj['paid_media'])
return cls(message_id, from_user, date, chat, content_type, opts, json_string)
@@ -1491,6 +1501,7 @@ def __init__(self, message_id, from_user, date, chat, content_type, options, jso
self.is_from_offline: Optional[bool] = None
self.effect_id: Optional[str] = None
self.show_caption_above_media: Optional[bool] = None
+ self.paid_media : Optional[PaidMediaInfo] = None
for key in options:
setattr(self, key, options[key])
@@ -6726,7 +6737,7 @@ def to_dict(self):
ret['height'] = self.height
if self.duration:
ret['duration'] = self.duration
- if self.supports_streaming:
+ if self.supports_streaming is not None:
ret['supports_streaming'] = self.supports_streaming
if self.has_spoiler is not None:
ret['has_spoiler'] = self.has_spoiler
@@ -7532,7 +7543,9 @@ class MenuButtonWebApp(MenuButton):
:type text: :obj:`str`
:param web_app: Description of the Web App that will be launched when the user presses the button. The Web App will be
- able to send an arbitrary message on behalf of the user using the method answerWebAppQuery.
+ able to send an arbitrary message on behalf of the user using the method answerWebAppQuery. Alternatively, a t.me link
+ to a Web App of the bot can be specified in the object instead of the Web App's URL, in which case the Web App will be
+ opened as if the user pressed the link.
:type web_app: :class:`telebot.types.WebAppInfo`
:return: Instance of the class
@@ -7735,8 +7748,11 @@ class InputFile:
InputFile(pathlib.Path('/path/to/file/file.txt'))
)
"""
- def __init__(self, file) -> None:
- self._file, self.file_name = self._resolve_file(file)
+ def __init__(self, file: Union[str, IOBase, Path], file_name: Optional[str] = None):
+ self._file, self._file_name = self._resolve_file(file)
+ if file_name:
+ self._file_name = file_name
+
@staticmethod
def _resolve_file(file):
@@ -7757,6 +7773,13 @@ def file(self):
File object.
"""
return self._file
+
+ @property
+ def file_name(self):
+ """
+ File name.
+ """
+ return self._file_name
class ForumTopicCreated(JsonDeserializable):
@@ -8511,6 +8534,9 @@ class ExternalReplyInfo(JsonDeserializable):
:param document: Optional. Message is a general file, information about the file
:type document: :class:`Document`
+ :param paid_media: Optional. Message is a paid media content
+ :type paid_media: :class:`PaidMedia`
+
:param photo: Optional. Message is a photo, available sizes of the photo
:type photo: :obj:`list` of :class:`PhotoSize`
@@ -8619,7 +8645,7 @@ def __init__(
dice: Optional[Dice]=None, game: Optional[Game]=None, giveaway: Optional[Giveaway]=None,
giveaway_winners: Optional[GiveawayWinners]=None, invoice: Optional[Invoice]=None,
location: Optional[Location]=None, poll: Optional[Poll]=None,
- venue: Optional[Venue]=None, **kwargs) -> None:
+ venue: Optional[Venue]=None, paid_media: Optional[PaidMediaInfo]=None, **kwargs) -> None:
self.origin: MessageOrigin = origin
self.chat: Optional[Chat] = chat
self.message_id: Optional[int] = message_id
@@ -8643,6 +8669,7 @@ def __init__(
self.location: Optional[Location] = location
self.poll: Optional[Poll] = poll
self.venue: Optional[Venue] = venue
+ self.paid_media: Optional[PaidMediaInfo] = paid_media
# noinspection PyUnresolvedReferences,PyShadowingBuiltins
@@ -10209,6 +10236,8 @@ def de_json(cls, json_string):
return TransactionPartnerFragment.de_json(obj)
elif obj["type"] == "user":
return TransactionPartnerUser.de_json(obj)
+ elif obj["type"] == "telegram_ads":
+ return TransactionPartnerTelegramAds.de_json(obj)
elif obj["type"] == "other":
return TransactionPartnerOther.de_json(obj)
@@ -10255,13 +10284,17 @@ class TransactionPartnerUser(TransactionPartner):
:param user: Information about the user
:type user: :class:`User`
+ :param invoice_payload: Optional, Bot-specified invoice payload
+ :type invoice_payload: :obj:`str`
+
:return: Instance of the class
:rtype: :class:`TransactionPartnerUser`
"""
- def __init__(self, type, user, **kwargs):
+ def __init__(self, type, user, invoice_payload=None, **kwargs):
self.type: str = type
self.user: User = user
+ self.invoice_payload: Optional[str] = invoice_payload
@classmethod
def de_json(cls, json_string):
@@ -10270,6 +10303,27 @@ def de_json(cls, json_string):
obj['user'] = User.de_json(obj['user'])
return cls(**obj)
+class TransactionPartnerTelegramAds(TransactionPartner):
+ """
+ Describes a transaction with Telegram Ads.
+
+ Telegram documentation: https://core.telegram.org/bots/api#transactionpartnertelegramads
+
+ :param type: Type of the transaction partner, always “telegram_ads”
+ :type type: :obj:`str`
+
+ :return: Instance of the class
+ :rtype: :class:`TransactionPartnerTelegramAds`
+ """
+
+ def __init__(self, type, **kwargs):
+ self.type: str = type
+
+ @classmethod
+ def de_json(cls, json_string):
+ if json_string is None: return None
+ obj = cls.check_json(json_string)
+
class TransactionPartnerOther(TransactionPartner):
"""
@@ -10361,3 +10415,266 @@ def de_json(cls, json_string):
def __init__(self, transactions, **kwargs):
self.transactions: List[StarTransaction] = transactions
+
+
+class PaidMedia(JsonDeserializable):
+ """
+ This object describes paid media. Currently, it can be one of
+
+ PaidMediaPreview
+ PaidMediaPhoto
+ PaidMediaVideo
+
+ Telegram documentation: https://core.telegram.org/bots/api#paidmedia
+
+ :return: Instance of the class
+ :rtype: :class:`PaidMediaPreview` or :class:`PaidMediaPhoto` or :class:`PaidMediaVideo`
+ """
+
+ @classmethod
+ def de_json(cls, json_string):
+ if json_string is None: return None
+ obj = cls.check_json(json_string)
+ if obj["type"] == "preview":
+ return PaidMediaPreview.de_json(obj)
+ elif obj["type"] == "photo":
+ return PaidMediaPhoto.de_json(obj)
+ elif obj["type"] == "video":
+ return PaidMediaVideo.de_json(obj)
+
+class PaidMediaPreview(PaidMedia):
+ """
+ The paid media isn't available before the payment.
+
+ Telegram documentation: https://core.telegram.org/bots/api#paidmediapreview
+
+ :param type: Type of the paid media, always “preview”
+ :type type: :obj:`str`
+
+ :param width: Optional. Media width as defined by the sender
+ :type width: :obj:`int`
+
+ :param height: Optional. Media height as defined by the sender
+ :type height: :obj:`int`
+
+ :param duration: Optional. Duration of the media in seconds as defined by the sender
+ :type duration: :obj:`int`
+
+ :return: Instance of the class
+ :rtype: :class:`PaidMediaPreview`
+ """
+
+ def __init__(self, type, width=None, height=None, duration=None, **kwargs):
+ self.type: str = type
+ self.width: Optional[int] = width
+ self.height: Optional[int] = height
+ self.duration: Optional[int] = duration
+
+ @classmethod
+ def de_json(cls, json_string):
+ if json_string is None: return None
+ obj = cls.check_json(json_string)
+ return cls(**obj)
+
+
+class PaidMediaPhoto(PaidMedia):
+ """
+ The paid media is a photo.
+
+ Telegram documentation: https://core.telegram.org/bots/api#paidmediaphoto
+
+ :param type: Type of the paid media, always “photo”
+ :type type: :obj:`str`
+
+ :param photo: The photo
+ :type photo: :obj:`list` of :class:`PhotoSize`
+
+ :return: Instance of the class
+ :rtype: :class:`PaidMediaPhoto`
+
+ """
+
+ def __init__(self, type, photo, **kwargs):
+ self.type: str = type
+ self.photo: List[PhotoSize] = photo
+
+ @classmethod
+ def de_json(cls, json_string):
+ if json_string is None: return None
+ obj = cls.check_json(json_string)
+
+ obj['photo'] = [PhotoSize.de_json(photo) for photo in obj['photo']]
+ return cls(**obj)
+
+
+class PaidMediaVideo(PaidMedia):
+ """
+ The paid media is a video.
+
+ Telegram documentation: https://core.telegram.org/bots/api#paidmediavideo
+
+ :param type: Type of the paid media, always “video”
+ :type type: :obj:`str`
+
+ :param video: The video
+ :type video: :class:`Video`
+
+ :return: Instance of the class
+ :rtype: :class:`PaidMediaVideo`
+ """
+
+ def __init__(self, type, video, **kwargs):
+ self.type: str = type
+ self.video: Video = video
+
+ @classmethod
+ def de_json(cls, json_string):
+ if json_string is None: return None
+ obj = cls.check_json(json_string)
+ obj['video'] = Video.de_json(obj['video'])
+ return cls(**obj)
+
+
+class PaidMediaInfo(JsonDeserializable):
+ """
+ Describes the paid media added to a message.
+
+ Telegram documentation: https://core.telegram.org/bots/api#paidmediainfo
+
+ :param star_count: The number of Telegram Stars that must be paid to buy access to the media
+ :type star_count: :obj:`int`
+
+ :param paid_media: Information about the paid media
+ :type paid_media: :obj:`list` of :class:`PaidMedia`
+
+ :return: Instance of the class
+ :rtype: :class:`PaidMediaInfo`
+ """
+
+ @classmethod
+ def de_json(cls, json_string):
+ if json_string is None: return None
+ obj = cls.check_json(json_string)
+ obj['paid_media'] = [PaidMedia.de_json(media) for media in obj['paid_media']]
+ return cls(**obj)
+
+ def __init__(self, star_count, paid_media, **kwargs):
+ self.star_count: int = star_count
+ self.paid_media: List[PaidMedia] = paid_media
+
+
+class InputPaidMedia(JsonSerializable):
+ """
+ This object describes the paid media to be sent. Currently, it can be one of
+ InputPaidMediaPhoto
+ InputPaidMediaVideo
+
+ Telegram documentation: https://core.telegram.org/bots/api#inputpaidmedia
+
+ :return: Instance of the class
+ :rtype: :class:`InputPaidMediaPhoto` or :class:`InputPaidMediaVideo`
+ """
+
+ def __init__(self, type, media, **kwargs):
+ self.type = type
+ self.media = media
+
+ if service_utils.is_string(self.media):
+ self._media_name = ''
+ self._media_dic = self.media
+ else:
+ self._media_name = service_utils.generate_random_token()
+ self._media_dic = 'attach://{0}'.format(self._media_name)
+
+ def to_json(self):
+ return json.dumps(self.to_dict())
+
+ def to_dict(self):
+ data = {
+ 'type': self.type,
+ 'media': self._media_dic
+ }
+ return data
+
+class InputPaidMediaPhoto(InputPaidMedia):
+ """
+ The paid media to send is a photo.
+
+ Telegram documentation: https://core.telegram.org/bots/api#inputpaidmediaphoto
+
+ :param type: Type of the media, must be photo
+ :type type: :obj:`str`
+
+ :param media: File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for
+ Telegram to get a file from the Internet, or pass “attach://” to upload a new one using multipart/form-data
+ under name. More information on Sending Files »
+ :type media: :obj:`str`
+
+ :return: Instance of the class
+ :rtype: :class:`InputPaidMediaPhoto`
+ """
+
+ def __init__(self, media, **kwargs):
+ super().__init__(type='photo', media=media)
+
+class InputPaidMediaVideo(InputPaidMedia):
+ """
+ The paid media to send is a video.
+
+ Telegram documentation: https://core.telegram.org/bots/api#inputpaidmediavideo
+
+ :param type: Type of the media, must be video
+ :type type: :obj:`str`
+
+ :param media: File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for
+ Telegram to get a file from the Internet, or pass “attach://” to upload a new one using multipart/form-data
+ under name. More information on Sending Files »
+ :type media: :obj:`str`
+
+ :param thumbnail: Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side.
+ The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320.
+ Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file,
+ so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under .
+ More information on Sending Files »
+ :type thumbnail: :class:`InputFile`
+
+ :param width: Optional. Video width
+ :type width: :obj:`int`
+
+ :param height: Optional. Video height
+ :type height: :obj:`int`
+
+ :param duration: Optional. Video duration in seconds
+ :type duration: :obj:`int`
+
+ :param supports_streaming: Optional. Pass True if the uploaded video is suitable for streaming
+ :type supports_streaming: :obj:`bool`
+
+ :return: Instance of the class
+ :rtype: :class:`InputPaidMediaVideo`
+
+ """
+
+ def __init__(self, media, thumbnail=None, width=None, height=None, duration=None, supports_streaming=None, **kwargs):
+ super().__init__(type='video', media=media)
+ self.thumbnail = thumbnail
+ self.width = width
+ self.height = height
+ self.duration = duration
+ self.supports_streaming = supports_streaming
+
+ def to_dict(self):
+ data = super().to_dict()
+ if self.thumbnail:
+ data['thumbnail'] = self.thumbnail
+ if self.width:
+ data['width'] = self.width
+ if self.height:
+ data['height'] = self.height
+ if self.duration:
+ data['duration'] = self.duration
+ if self.supports_streaming is not None:
+ data['supports_streaming'] = self.supports_streaming
+ return data
+
+
\ No newline at end of file
diff --git a/telebot/version.py b/telebot/version.py
index 96c6fbd20..5016764c6 100644
--- a/telebot/version.py
+++ b/telebot/version.py
@@ -1,3 +1,3 @@
# Versions should comply with PEP440.
# This line is parsed in setup.py:
-__version__ = '4.20.0'
+__version__ = '4.21.0'