diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 00000000..3837c837 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,98 @@ +name: Bug report +description: Report issues affecting the framework or the documentation. +labels: + - bug +body: + - type: checkboxes + attributes: + label: Checklist + options: + - label: I am sure the error is coming from aiogram code + required: true + - label: I have searched in the issue tracker for similar bug reports, including closed ones + required: true + + - type: markdown + attributes: + value: | + ## Context + + Please provide as much information as possible. This will help us to reproduce the issue and fix it. + + - type: input + attributes: + label: Operating system + placeholder: e.g. Ubuntu 20.04.2 LTS + validations: + required: true + + - type: input + attributes: + label: Python version + placeholder: e.g. 3.10.1 + validations: + required: true + + - type: input + attributes: + label: aiogram version + placeholder: e.g. 2.21 or 3.0b3 + validations: + required: true + + - type: textarea + attributes: + label: Expected behavior + description: Please describe the behavior you are expecting. + placeholder: E.g. the bot should send a message with the text "Hello, world!". + validations: + required: true + + - type: textarea + attributes: + label: Current behavior + description: Please describe the behavior you are currently experiencing. + placeholder: E.g. the bot doesn't send any message. + validations: + required: true + + - type: textarea + attributes: + label: Steps to reproduce + description: Please describe the steps you took to reproduce the behavior. + placeholder: | + 1. step 1 + 2. step 2 + 3. ... + 4. you get it... + validations: + required: true + + - type: textarea + attributes: + label: Code example + description: Provide a [minimal, reproducible](https://stackoverflow.com/help/minimal-reproducible-example) and properly formatted example (if applicable). + placeholder: | + from aiogram import Bot, Dispatcher + ... + render: python3 + + - type: textarea + attributes: + label: Logs + description: Provide the complete traceback (if applicable) or other kind of logs. + placeholder: | + Traceback (most recent call last): + File "main.py", line 1, in + ... + SomeException: ... + render: sh + + - type: textarea + attributes: + label: Additional information + description: Please provide any additional information that may help us to reproduce the issue. + placeholder: | + E.g. this behavior is reproducible only in group chats. + + You can also attach additional screenshots, logs, or other files. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 2a1994a5..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- - ---- -name: Bug report -about: Create a report to help us improve - ---- - -## Context - -Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions. - -* Operating System: -* Python Version: -* aiogram version: -* aiohttp version: -* uvloop version (if installed): - -## Expected Behavior - -Please describe the behavior you are expecting - -## Current Behavior - -What is the current behavior? - -## Failure Information (for bugs) - -Please help provide information about the failure if this is a bug. If it is not a bug, please remove the rest of this template. - -### Steps to Reproduce - -Please provide detailed steps for reproducing the issue. - -1. step 1 -2. step 2 -3. you get it... - -### Failure Logs - -Please include any relevant log snippets or files here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..aee2a9d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: true +contact_links: + - name: Discuss anything related to the framework + url: https://github.com/aiogram/aiogram/discussions + about: Ask a question about aiogram or share your code snippets and ideas. + - name: Join our Telegram channel + url: https://t.me/aiogram_live + about: Get the latest updates about the framework. + - name: Join our Telegram chat + url: https://t.me/aiogram + about: Get help, ask questions, and discuss the framework in real-time. diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 00000000..7fbd1f37 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,44 @@ +name: Feature request +description: Report features you would like to see or improve in the framework. +labels: + - enhancement +body: + - type: textarea + attributes: + label: Problem + description: Is your feature request related to a specific problem? If not, please describe the general idea of your request. + placeholder: e.g. I want to send a photo to a user by url. + validations: + required: true + + - type: textarea + attributes: + label: Possible solution + description: Describe the solution you would like to see in the framework. + placeholder: e.g. Add a method to send a photo to a user by url. + validations: + required: true + + - type: textarea + attributes: + label: Alternatives + description: What other solutions do you have in mind? + placeholder: e.g. I'm sending a text message with photo url. + + - type: textarea + attributes: + label: Code example + description: A small code example that demonstrates the behavior you would like to see. + placeholder: | + await bot.send_photo(user_id, photo_url) + ... + render: python3 + + - type: textarea + attributes: + label: Additional information + description: Any additional information you would like to provide. + placeholder: | + E.g. this method should also cache images to speed up further sending. + + You can also attach additional pictures or other files. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 066b2d92..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1c33729f..cd082ad2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,35 +1,52 @@ # Description -Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. + + +Type here... ## Type of change + -- [ ] Documentation (typos, code examples or any documentation update) -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] This change requires a documentation update +- Breaking change (fix or feature that would cause existing functionality to not work as expected) +- Bug fix (non-breaking change that fixes an issue) +- New feature (non-breaking change that adds functionality) +- Documentation (typos, code examples or any documentation update) -# How Has This Been Tested? +# How has this been tested? -Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + -- [ ] Test A -- [ ] Test B +Type here... -**Test Configuration**: -* Operating System: -* Python version: +## Test Configuration +- Operating system: e.g. Ubuntu 20.04.2 LTS +-Python version: e.g. 3.10.1 # Checklist: + + - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code -- [ ] I have made corresponding changes to the documentation -- [ ] My changes generate no new warnings -- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have added tests that prove my fix is effective or that my feature works as expected - [ ] New and existing unit tests pass locally with my changes +- [ ] My changes generate no new warnings or errors +- [ ] My changes are compatible with minimum requirements of the project +- [ ] I have made corresponding changes to the documentation diff --git a/.github/workflows/label_pr.yaml b/.github/workflows/label_pr.yaml new file mode 100644 index 00000000..2b58136a --- /dev/null +++ b/.github/workflows/label_pr.yaml @@ -0,0 +1,17 @@ +name: Label new pull request + +on: + pull_request_target: + types: + - opened + branches: + - dev-2.x + +jobs: + put-label: + runs-on: ubuntu-latest + steps: + - name: Add 2.x label + uses: andymckay/labeler@master + with: + add-labels: 2.x diff --git a/Makefile b/Makefile index 662ad49f..51d5af96 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VENV_NAME := venv +VENV_NAME := .venv PYTHON := $(VENV_NAME)/bin/python AIOGRAM_VERSION := $(shell $(PYTHON) -c "import aiogram;print(aiogram.__version__)") diff --git a/README.md b/README.md index 75736386..fbc5c5bf 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.7-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-6.2-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/README.rst b/README.rst index 47a7cb9d..ee48c1be 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ AIOGramBot :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.7-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.2-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 6c8d3f4c..7ae9bfd2 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.19' -__api_version__ = '5.7' +__version__ = '2.22' +__api_version__ = '6.2' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 5f609227..f287e32f 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -269,6 +269,7 @@ class Methods(Helper): SEND_STICKER = Item() # sendSticker GET_STICKER_SET = Item() # getStickerSet UPLOAD_STICKER_FILE = Item() # uploadStickerFile + GET_CUSTOM_EMOJI_STICKERS = Item() # getCustomEmojiStickers CREATE_NEW_STICKER_SET = Item() # createNewStickerSet ADD_STICKER_TO_SET = Item() # addStickerToSet SET_STICKER_POSITION_IN_SET = Item() # setStickerPositionInSet @@ -278,8 +279,16 @@ class Methods(Helper): # Inline mode ANSWER_INLINE_QUERY = Item() # answerInlineQuery + ANSWER_WEB_APP_QUERY = Item() # answerWebAppQuery + SET_CHAT_MENU_BUTTON = Item() # setChatMenuButton + GET_CHAT_MENU_BUTTON = Item() # getChatMenuButton + + SET_MY_DEFAULT_ADMINISTRATOR_RIGHTS = Item() # setMyDefaultAdministratorRights + GET_MY_DEFAULT_ADMINISTRATOR_RIGHTS = Item() # getMyDefaultAdministratorRights + # Payments SEND_INVOICE = Item() # sendInvoice + CREATE_INVOICE_LINK = Item() # createInvoiceLink ANSWER_SHIPPING_QUERY = Item() # answerShippingQuery ANSWER_PRE_CHECKOUT_QUERY = Item() # answerPreCheckoutQuery diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 8fd20949..0c236681 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -38,6 +38,7 @@ class BaseBot: validate_token: Optional[base.Boolean] = True, parse_mode: typing.Optional[base.String] = None, disable_web_page_preview: Optional[base.Boolean] = None, + protect_content: Optional[base.Boolean] = None, timeout: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] = None, server: TelegramAPIServer = TELEGRAM_PRODUCTION ): @@ -60,6 +61,9 @@ class BaseBot: :type parse_mode: :obj:`str` :param disable_web_page_preview: You can set default disable web page preview parameter :type disable_web_page_preview: :obj:`bool` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` :param timeout: Request timeout :type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]` :param server: Telegram Bot API Server endpoint. @@ -111,6 +115,7 @@ class BaseBot: self.parse_mode = parse_mode self.disable_web_page_preview = disable_web_page_preview + self.protect_content = protect_content async def get_new_session(self) -> aiohttp.ClientSession: return aiohttp.ClientSession( @@ -361,5 +366,22 @@ class BaseBot: def disable_web_page_preview(self): self.disable_web_page_preview = None + @property + def protect_content(self): + return getattr(self, "_protect_content", None) + + @protect_content.setter + def protect_content(self, value): + if value is None: + setattr(self, "_protect_content", None) + return + if not isinstance(value, bool): + raise TypeError(f"Protect content must be bool, not {type(value)}") + setattr(self, "_protect_content", value) + + @protect_content.deleter + def protect_content(self): + self.protect_content = None + def check_auth_widget(self, data): return check_integrity(self.__token, data) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 91c0b283..27c1ed63 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -8,7 +8,7 @@ import warnings from .base import BaseBot, api from .. import types from ..types import base -from ..utils.deprecated import deprecated, removed_argument +from ..utils.deprecated import deprecated from ..utils.exceptions import ValidationError from ..utils.mixins import DataMixin, ContextInstanceMixin from ..utils.payload import generate_payload, prepare_arg, prepare_attachment, prepare_file @@ -117,6 +117,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): max_connections: typing.Optional[base.Integer] = None, allowed_updates: typing.Optional[typing.List[base.String]] = None, drop_pending_updates: typing.Optional[base.Boolean] = None, + secret_token: typing.Optional[str] = None, ) -> base.Boolean: """ Use this method to specify a url and receive incoming updates via an outgoing @@ -165,6 +166,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param drop_pending_updates: Pass True to drop all pending updates :type drop_pending_updates: :obj:`typing.Optional[base.Boolean]` + :param secret_token: A secret token to be sent in a header “X-Telegram-Bot-Api-Secret-Token” + in every webhook request, 1-256 characters. Only characters A-Z, a-z, 0-9, _ and - are allowed. + The header is useful to ensure that the request comes from a webhook set by you. + :type secret_token: :obj:`typing.Optional[str]` :return: Returns true :rtype: :obj:`base.Boolean` """ @@ -330,6 +335,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload.setdefault('parse_mode', self.parse_mode) if self.disable_web_page_preview: payload.setdefault('disable_web_page_preview', self.disable_web_page_preview) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) result = await self.request(api.Methods.SEND_MESSAGE, payload) return types.Message(**result) @@ -370,6 +377,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`types.Message` """ payload = generate_payload(**locals()) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) result = await self.request(api.Methods.FORWARD_MESSAGE, payload) return types.Message(**result) @@ -452,6 +461,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals()) if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) result = await self.request(api.Methods.COPY_MESSAGE, payload) return types.MessageId(**result) @@ -520,6 +531,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals(), exclude=['photo']) if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) files = {} prepare_file(payload, files, 'photo', photo) @@ -610,6 +623,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals(), exclude=['audio', 'thumb']) if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) files = {} prepare_file(payload, files, 'audio', audio) @@ -700,6 +715,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals(), exclude=['document']) if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) files = {} prepare_file(payload, files, 'document', document) @@ -792,6 +809,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals(), exclude=['video', 'thumb']) if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) files = {} prepare_file(payload, files, 'video', video) @@ -887,6 +906,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals(), exclude=["animation", "thumb"]) if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) files = {} prepare_file(payload, files, 'animation', animation) @@ -967,6 +988,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals(), exclude=['voice']) if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) files = {} prepare_file(payload, files, 'voice', voice) @@ -1033,6 +1056,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals(), exclude=['video_note']) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) files = {} prepare_file(payload, files, 'video_note', video_note) @@ -1089,12 +1114,15 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): # Check MediaGroup quantity if not (1 <= len(media.media) <= 10): - raise ValidationError("Media group must include 2-10 items as written in docs, but also it works with 1 element") + raise ValidationError( + "Media group must include 2-10 items as written in docs, but also it works with 1 element") files = dict(media.get_files()) media = prepare_arg(media) payload = generate_payload(**locals(), exclude=['files']) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) result = await self.request(api.Methods.SEND_MEDIA_GROUP, payload, files) return [types.Message(**message) for message in result] @@ -1168,6 +1196,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) result = await self.request(api.Methods.SEND_LOCATION, payload) return types.Message(**result) @@ -1346,6 +1376,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) result = await self.request(api.Methods.SEND_VENUE, payload) return types.Message(**result) @@ -1409,6 +1441,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals()) result = await self.request(api.Methods.SEND_CONTACT, payload) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) return types.Message(**result) async def send_poll(self, @@ -1527,6 +1561,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals()) if self.parse_mode and explanation_entities is None: payload.setdefault('explanation_parse_mode', self.parse_mode) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) result = await self.request(api.Methods.SEND_POLL, payload) return types.Message(**result) @@ -1586,6 +1622,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) result = await self.request(api.Methods.SEND_DICE, payload) return types.Message(**result) @@ -1704,9 +1742,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): datetime.datetime, datetime.timedelta, None]` :param revoke_messages: Pass True to delete all messages from - the chat for the user that is being removed. If False, the user - will be able to see messages in the group that were sent before - the user was removed. Always True for supergroups and channels. + the chat for the user that is being removed. If False, the user + will be able to see messages in the group that were sent before + the user was removed. Always True for supergroups and channels. :type revoke_messages: :obj:`typing.Optional[base.Boolean]` :return: Returns True on success @@ -1834,6 +1872,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): can_restrict_members: typing.Optional[base.Boolean] = None, can_pin_messages: typing.Optional[base.Boolean] = None, can_promote_members: typing.Optional[base.Boolean] = None, + can_manage_video_chats: typing.Optional[base.Boolean] = None, ) -> base.Boolean: """ Use this method to promote or demote a user in a supergroup or a channel. @@ -1885,9 +1924,17 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): directly or indirectly (promoted by administrators that were appointed by him) :type can_promote_members: :obj:`typing.Optional[base.Boolean]` + :param can_manage_video_chats: Pass True, if the administrator can manage video chats + :return: Returns True on success :rtype: :obj:`base.Boolean` """ + if can_manage_voice_chats: + warnings.warn( + "Argument `can_manage_voice_chats` was renamed to `can_manage_video_chats` and will be removed in aiogram 2.21") + can_manage_video_chats = can_manage_voice_chats + can_manage_voice_chats = None + payload = generate_payload(**locals()) return await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload) @@ -1910,11 +1957,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return await self.request(api.Methods.SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE, payload) - @removed_argument("until_date", "2.19") async def ban_chat_sender_chat( - self, - chat_id: typing.Union[base.Integer, base.String], - sender_chat_id: base.Integer, + self, + chat_id: typing.Union[base.Integer, base.String], + sender_chat_id: base.Integer, ): """Ban a channel chat in a supergroup or a channel. @@ -1937,9 +1983,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return await self.request(api.Methods.BAN_CHAT_SENDER_CHAT, payload) async def unban_chat_sender_chat( - self, - chat_id: typing.Union[base.Integer, base.String], - sender_chat_id: base.Integer, + self, + chat_id: typing.Union[base.Integer, base.String], + sender_chat_id: base.Integer, ): """Unban a previously banned channel chat in a supergroup or channel. @@ -2587,6 +2633,87 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.GET_MY_COMMANDS, payload) return [types.BotCommand(**bot_command_data) for bot_command_data in result] + async def set_chat_menu_button(self, chat_id: typing.Optional[base.Integer] = None, + menu_button: typing.Optional[types.MenuButton] = None) -> bool: + """ + Use this method to change bot's menu button in a private chat, or the default menu button. + + Returns True on success. + + Source https://core.telegram.org/bots/api#setchatmenubutton + + :param chat_id: Unique identifier for the target private chat. + If not specified, default bot's menu button will be changed + :param menu_button: + A JSON-serialized object for the new bot's menu button. Defaults to MenuButtonDefault + :return: Returns True on success. + """ + menu_button = prepare_arg(menu_button) + payload = generate_payload(**locals()) + + return await self.request(api.Methods.SET_CHAT_MENU_BUTTON, payload) + + async def get_chat_menu_button(self, chat_id: typing.Optional[base.Integer] = None) -> typing.Union[ + "types.MenuButtonCommands", + "types.MenuButtonDefault", + "types.MenuButtonWebApp", + ]: + """ + Use this method to get the current value of the bot's menu button in a private chat, + or the default menu button. + + Returns MenuButton on success. + + Source https://core.telegram.org/bots/api#getchatmenu + + :param chat_id: Unique identifier for the target private chat. If not specified, + default bot's menu button will be returned + :return: Returns MenuButton on success. + """ + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.GET_CHAT_MENU_BUTTON, payload) + return types.MenuButton.resolve(**result) + + async def set_my_default_administrator_rights(self, rights: typing.Optional[types.ChatAdministratorRights] = None, + for_channels: typing.Optional[base.Boolean] = None) -> base.Boolean: + """ + Use this method to change default administrator rights of the bot for adding it as an administrator + to groups or channels. + Returns True on success. + + Source: https://core.telegram.org/bots/api#setmydefaultadministratorrights + + :param rights: A JSON-serialized object, describing new default administrator rights. + If not specified, the default administrator rights will be cleared. + :param for_channels: + Pass True to change default administrator rights of the bot in channels. + Otherwise, default administrator rights of the bot for groups and supergroups will be changed. + :return: Returns True on success. + """ + rights = prepare_arg(rights) + payload = generate_payload(**locals()) + + return await self.request(api.Methods.SET_MY_DEFAULT_ADMINISTRATOR_RIGHTS, payload) + + async def get_my_default_administrator_rights(self, + for_channels: typing.Optional[base.Boolean] = None + ) -> types.ChatAdministratorRights: + """ + Use this method to get the current default administrator rights of the bot. + Returns ChatAdministratorRights on success. + + Source: https://core.telegram.org/bots/api#getmydefaultadministratorrights + + :param for_channels: Pass True to get default administrator rights of the bot in channels. + Otherwise, default administrator rights of the bot for groups and supergroups will be returned. + :return: + """ + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.GET_MY_DEFAULT_ADMINISTRATOR_RIGHTS, payload) + return types.ChatAdministratorRights(**result) + async def edit_message_text(self, text: base.String, chat_id: typing.Union[base.Integer, base.String, None] = None, @@ -2871,6 +2998,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals(), exclude=['sticker']) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) files = {} prepare_file(payload, files, 'sticker', sticker) @@ -2917,6 +3046,23 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.UPLOAD_STICKER_FILE, payload, files) return types.File(**result) + async def get_custom_emoji_stickers(self, custom_emoji_ids: typing.List[base.String]) -> typing.List[types.Sticker]: + """ + Use this method to get information about custom emoji stickers by their identifiers. + + + Source: https://core.telegram.org/bots/api#uploadstickerfile + + :param custom_emoji_ids: User identifier of sticker file owner + :type custom_emoji_ids: :obj:`typing.List[base.String]` + :return: Returns an Array of Sticker objects. + :rtype: :obj:`typing.List[types.Sticker]` + """ + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.GET_CUSTOM_EMOJI_STICKERS, payload) + return [types.Sticker(**item) for item in result] + async def create_new_sticker_set(self, user_id: base.Integer, name: base.String, @@ -2926,6 +3072,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): tgs_sticker: base.InputFile = None, webm_sticker: base.InputFile = None, contains_masks: typing.Optional[base.Boolean] = None, + sticker_type: typing.Optional[base.String] = None, mask_position: typing.Optional[types.MaskPosition] = None) -> base.Boolean: """ Use this method to create a new sticker set owned by a user. @@ -2954,7 +3101,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type tgs_sticker: :obj:`base.InputFile` :param webm_sticker: WEBM video with the sticker, uploaded using multipart/form-data. See https://core.telegram.org/stickers#video-sticker-requirements for technical requirements - :type webm_sticker: :obj:`base.InputFile` + :type webm_sticker: :obj:`base.String` + :param sticker_type: Type of stickers in the set, pass “regular” or “mask”. + Custom emoji sticker sets can't be created via the Bot API at the moment. + By default, a regular sticker set is created. + :type sticker_type: :obj:`base.InputFile` :param emojis: One or more emoji corresponding to the sticker :type emojis: :obj:`base.String` :param contains_masks: Pass True, if a set of mask stickers should be created @@ -2966,6 +3117,12 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ mask_position = prepare_arg(mask_position) payload = generate_payload(**locals(), exclude=['png_sticker', 'tgs_sticker', 'webm_sticker']) + if contains_masks is not None: + warnings.warn( + message="The parameter `contains_masks` deprecated, use `sticker_type` instead.", + category=DeprecationWarning, + stacklevel=2 + ) files = {} prepare_file(payload, files, 'png_sticker', png_sticker) @@ -3133,6 +3290,25 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return await self.request(api.Methods.ANSWER_INLINE_QUERY, payload) + async def answer_web_app_query(self, web_app_query_id: base.String, + result: types.InlineQueryResult) -> types.SentWebAppMessage: + """ + Use this method to set result of interaction with web app and send corresponding message + on behalf of the user to the chat from which the query originated. + On success, SentWebAppMessage is returned. + + Source https://core.telegram.org/bots/api#answerwebappquery + + :param web_app_query_id: Unique identifier for the answered query + :param result: A JSON-serialized object with a description of the message to send + :return: On success, SentWebAppMessage is returned. + """ + result = prepare_arg(result) + payload = generate_payload(**locals()) + + response = await self.request(api.Methods.ANSWER_WEB_APP_QUERY, payload) + return types.SentWebAppMessage(**response) + # === Payments === # https://core.telegram.org/bots/api#payments @@ -3284,10 +3460,75 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) provider_data = prepare_arg(provider_data) payload_ = generate_payload(**locals()) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) result = await self.request(api.Methods.SEND_INVOICE, payload_) return types.Message(**result) + async def create_invoice_link(self, + title: base.String, + description: base.String, + payload: base.String, + provider_token: base.String, + currency: base.String, + prices: typing.List[types.LabeledPrice], + max_tip_amount: typing.Optional[int] = None, + suggested_tip_amounts: typing.Optional[typing.List[int]] = None, + provider_data: typing.Optional[base.String] = None, + photo_url: typing.Optional[str] = None, + photo_size: typing.Optional[int] = None, + photo_width: typing.Optional[int] = None, + photo_height: typing.Optional[int] = None, + need_name: typing.Optional[bool] = None, + need_phone_number: typing.Optional[bool] = None, + need_email: typing.Optional[bool] = None, + need_shipping_address: typing.Optional[bool] = None, + send_phone_number_to_provider: typing.Optional[bool] = None, + send_email_to_provider: typing.Optional[bool] = None, + is_flexible: typing.Optional[bool] = None, + ) -> str: + """ + Use this method to create a link for an invoice. On success, the created link is returned. + + Source: https://core.telegram.org/bots/api#createinvoicelink + + :param title: Product name, 1-32 characters + :param description: Product description, 1-255 characters + :param payload: Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes. + :param provider_token: Payment provider token, obtained via BotFather + :param currency: Three-letter ISO 4217 currency code, see more on currencies + :param prices: Price breakdown, a JSON-serialized list of components + (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) + :param max_tip_amount: The maximum accepted amount for tips in the smallest units of the currency + (integer, not float/double). For example, for a maximum tip of US$ 1.45 pass max_tip_amount = 145. + See the exp parameter in currencies.json, it shows the number of digits past the decimal point for + each currency (2 for the majority of currencies). Defaults to 0 + :param suggested_tip_amounts: A JSON-serialized array of suggested amounts of tips in the smallest units + of the currency (integer, not float/double). At most 4 suggested tip amounts can be specified. + The suggested tip amounts must be positive, passed in a strictly increased order and must not + exceed max_tip_amount. + :param provider_data: JSON-serialized data about the invoice, which will be shared with the payment provider. + A detailed description of required fields should be provided by the payment provider. + :param photo_url: URL of the product photo for the invoice. + Can be a photo of the goods or a marketing image for a service. + :param photo_size: Photo size in bytes + :param photo_width: Photo width + :param photo_height: Photo height + :param need_name: Pass True, if you require the user's full name to complete the order + :param need_phone_number: Pass True, if you require the user's phone number to complete the order + :param need_email: Pass True, if you require the user's email address to complete the order + :param need_shipping_address: Pass True, if you require the user's shipping address to complete the order + :param send_phone_number_to_provider: Pass True, if the user's phone number should be sent to the provider + :param send_email_to_provider: Pass True, if the user's email address should be sent to the provider + :param is_flexible: Pass True, if the final price depends on the shipping method + :return: + """ + prices = prepare_arg([price.to_python() if hasattr(price, 'to_python') else price for price in prices]) + payload = generate_payload(**locals()) + + return await self.request(api.Methods.CREATE_INVOICE_LINK, payload) + async def answer_shipping_query(self, shipping_query_id: base.String, ok: base.Boolean, shipping_options: typing.Union[typing.List[types.ShippingOption], None] = None, error_message: typing.Optional[base.String] = None) -> base.Boolean: @@ -3426,6 +3667,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) + if self.protect_content is not None: + payload.setdefault('protect_content', self.protect_content) result = await self.request(api.Methods.SEND_GAME, payload) return types.Message(**result) diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py index 7a128f1c..6b7e61cd 100644 --- a/aiogram/contrib/fsm_storage/mongo.py +++ b/aiogram/contrib/fsm_storage/mongo.py @@ -213,5 +213,5 @@ class MongoStorage(BaseStorage): :return: list of tuples where first element is chat id and second is user id """ db = await self.get_db() - items = await db[STATE].find().to_list() + items = await db[STATE].find().to_list(length=None) return [(int(item['chat']), int(item['user'])) for item in items] diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 7abe1d8d..22f6700c 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -1367,7 +1367,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): bucket = await self.storage.get_bucket(chat=chat_id, user=user_id) if bucket and key in bucket: - del bucket['key'] + del bucket[key] await self.storage.set_bucket(chat=chat_id, user=user_id, bucket=bucket) return True return False diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index ebd38f08..e109eb6d 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -10,7 +10,7 @@ from babel.support import LazyProxy from aiogram import types from aiogram.dispatcher.filters.filters import BoundFilter, Filter -from aiogram.types import CallbackQuery, ChatType, InlineQuery, Message, Poll, ChatMemberUpdated +from aiogram.types import CallbackQuery, ChatType, InlineQuery, Message, Poll, ChatMemberUpdated, BotCommand ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] @@ -34,7 +34,7 @@ class Command(Filter): By default this filter is registered for messages and edited messages handlers. """ - def __init__(self, commands: Union[Iterable, str], + def __init__(self, commands: Union[Iterable[Union[str, BotCommand]], str, BotCommand], prefixes: Union[Iterable, str] = '/', ignore_case: bool = True, ignore_mention: bool = False, @@ -66,8 +66,19 @@ class Command(Filter): @dp.message_handler(commands=['myCommand'], commands_ignore_caption=False, content_types=ContentType.ANY) @dp.message_handler(Command(['myCommand'], ignore_caption=False), content_types=[ContentType.TEXT, ContentType.DOCUMENT]) """ - if isinstance(commands, str): + if isinstance(commands, (str, BotCommand)): commands = (commands,) + elif isinstance(commands, Iterable): + if not all(isinstance(cmd, (str, BotCommand)) for cmd in commands): + raise ValueError( + "Command filter only supports str, BotCommand object or their Iterable" + ) + else: + raise ValueError( + "Command filter doesn't support {} as input. " + "It only supports str, BotCommand object or their Iterable".format(type(commands)) + ) + commands = [cmd.command if isinstance(cmd, BotCommand) else cmd for cmd in commands] self.commands = list(map(str.lower, commands)) if ignore_case else commands self.prefixes = prefixes @@ -728,18 +739,20 @@ class ChatTypeFilter(BoundFilter): self.chat_type: typing.Set[str] = set(chat_type) - async def check(self, obj: Union[Message, CallbackQuery, ChatMemberUpdated]): + async def check(self, obj: Union[Message, CallbackQuery, ChatMemberUpdated, InlineQuery]): if isinstance(obj, Message): - obj = obj.chat + chat_type = obj.chat.type elif isinstance(obj, CallbackQuery): - obj = obj.message.chat + chat_type = obj.message.chat.type if obj.message else None elif isinstance(obj, ChatMemberUpdated): - obj = obj.chat + chat_type = obj.chat.type + elif isinstance(obj, InlineQuery): + chat_type = obj.chat_type else: warnings.warn("ChatTypeFilter doesn't support %s as input", type(obj)) return False - return obj.type in self.chat_type + return chat_type in self.chat_type class MediaGroupFilter(BoundFilter): diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 10a94924..85d7731d 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -76,8 +76,8 @@ class Handler: """ for handler_obj in self.handlers: registered = handler_obj.handler - if handler is registered: - self.handlers.remove(handler_obj) + if handler in self.handlers: + self.handlers.remove(handler) return True raise ValueError('This handler is not registered!') diff --git a/aiogram/dispatcher/storage.py b/aiogram/dispatcher/storage.py index 63ce25b2..dcc42ed8 100644 --- a/aiogram/dispatcher/storage.py +++ b/aiogram/dispatcher/storage.py @@ -271,7 +271,7 @@ class BaseStorage: :param user: :return: """ - await self.set_data(chat=chat, user=user, data={}) + await self.set_bucket(chat=chat, user=user, bucket={}) @staticmethod def resolve_state(value): diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 92e475ff..32987899 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -448,6 +448,29 @@ class DisableWebPagePreviewMixin: return bot.disable_web_page_preview +class ProtectContentMixin: + def protect_content(self): + """ + Protect content + + :return: + """ + setattr(self, "protect_content", True) + return self + + @staticmethod + def _global_protect_content(): + """ + Detect global protect content value + + :return: + """ + from aiogram import Bot + bot = Bot.get_current() + if bot is not None: + return bot.protect_content + + class ParseModeMixin: def as_html(self): """ @@ -480,7 +503,9 @@ class ParseModeMixin: return bot.parse_mode -class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificationMixin, DisableWebPagePreviewMixin): +class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, + DisableNotificationMixin, DisableWebPagePreviewMixin, + ProtectContentMixin): """ You can send message with webhook by using this instance of this object. All arguments is equal with Bot.send_message method. @@ -488,7 +513,7 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio __slots__ = ('chat_id', 'text', 'parse_mode', 'disable_web_page_preview', 'disable_notification', - 'reply_to_message_id', 'reply_markup') + 'protect_content', 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_MESSAGE @@ -497,6 +522,7 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio parse_mode: Optional[String] = None, disable_web_page_preview: Optional[Boolean] = None, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None): @@ -509,6 +535,8 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio :param disable_web_page_preview: Boolean (Optional) - Disables link previews for links in this message :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, @@ -520,12 +548,15 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio parse_mode = self._global_parse_mode() if disable_web_page_preview is None: disable_web_page_preview = self._global_disable_web_page_preview() + if protect_content is None: + protect_content = self._global_protect_content() self.chat_id = chat_id self.text = text self.parse_mode = parse_mode self.disable_web_page_preview = disable_web_page_preview self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -536,6 +567,7 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio 'parse_mode': self.parse_mode, 'disable_web_page_preview': self.disable_web_page_preview, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } @@ -565,18 +597,19 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio return self -class ForwardMessage(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class ForwardMessage(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for forward messages of any kind on to webhook. """ - __slots__ = ('chat_id', 'from_chat_id', 'message_id', 'disable_notification') + __slots__ = ('chat_id', 'from_chat_id', 'message_id', 'disable_notification', 'protect_content') method = api.Methods.FORWARD_MESSAGE def __init__(self, chat_id: Union[Integer, String] = None, from_chat_id: Union[Integer, String] = None, message_id: Integer = None, - disable_notification: Optional[Boolean] = None): + disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None): """ :param chat_id: Union[Integer, String] - Unique identifier for the target chat or username of the target channel (in the format @channelusername) @@ -584,12 +617,18 @@ class ForwardMessage(BaseResponse, ReplyToMixin, DisableNotificationMixin): message was sent (or channel username in the format @channelusername) :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param message_id: Integer - Message identifier in the chat specified in from_chat_id """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.from_chat_id = from_chat_id self.message_id = message_id self.disable_notification = disable_notification + self.protect_content = protect_content def message(self, message: types.Message): """ @@ -607,16 +646,18 @@ class ForwardMessage(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'chat_id': self.chat_id, 'from_chat_id': self.from_chat_id, 'message_id': self.message_id, - 'disable_notification': self.disable_notification + 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, } -class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for send photo on to webhook. """ - __slots__ = ('chat_id', 'photo', 'caption', 'disable_notification', 'reply_to_message_id', 'reply_markup') + __slots__ = ('chat_id', 'photo', 'caption', 'disable_notification', + 'protect_content', 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_PHOTO @@ -624,6 +665,7 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin): photo: String, caption: Optional[String] = None, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None): @@ -637,15 +679,21 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin): 0-1024 characters after entities parsing :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.photo = photo self.caption = caption self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -655,18 +703,20 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'photo': self.photo, 'caption': self.caption, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } -class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for send audio on to webhook. """ __slots__ = ('chat_id', 'audio', 'caption', 'duration', 'performer', 'title', - 'disable_notification', 'reply_to_message_id', 'reply_markup') + 'disable_notification', 'protect_content', + 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_AUDIO @@ -677,6 +727,7 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin): performer: Optional[String] = None, title: Optional[String] = None, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None): @@ -693,11 +744,16 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin): :param title: String (Optional) - Track name :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.audio = audio self.caption = caption @@ -705,6 +761,7 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin): self.performer = performer self.title = title self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -717,17 +774,19 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'performer': self.performer, 'title': self.title, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } -class SendDocument(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendDocument(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for send document on to webhook. """ - __slots__ = ('chat_id', 'document', 'caption', 'disable_notification', 'reply_to_message_id', 'reply_markup') + __slots__ = ('chat_id', 'document', 'caption', 'disable_notification', + 'protect_content', 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_DOCUMENT @@ -735,6 +794,7 @@ class SendDocument(BaseResponse, ReplyToMixin, DisableNotificationMixin): document: String, caption: Optional[String] = None, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None): @@ -749,15 +809,21 @@ class SendDocument(BaseResponse, ReplyToMixin, DisableNotificationMixin): (may also be used when resending documents by file_id), 0-1024 characters after entities parsing :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.document = document self.caption = caption self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -767,17 +833,19 @@ class SendDocument(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'document': self.document, 'caption': self.caption, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } -class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for send video on to webhook. """ - __slots__ = ('chat_id', 'video', 'duration', 'width', 'height', 'caption', 'disable_notification', + __slots__ = ('chat_id', 'video', 'duration', 'width', 'height', 'caption', + 'disable_notification', 'protect_content', 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_VIDEO @@ -789,6 +857,7 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin): height: Optional[Integer] = None, caption: Optional[String] = None, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None): @@ -806,11 +875,16 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin): 0-1024 characters after entities parsing :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.video = video self.duration = duration @@ -818,6 +892,7 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin): self.height = height self.caption = caption self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -830,18 +905,19 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'height': self.height, 'caption': self.caption, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } -class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for send voice on to webhook. """ __slots__ = ('chat_id', 'voice', 'caption', 'duration', 'disable_notification', - 'reply_to_message_id', 'reply_markup') + 'protect_content', 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_VOICE @@ -850,6 +926,7 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): caption: Optional[String] = None, duration: Optional[Integer] = None, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None): @@ -864,16 +941,22 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): :param duration: Integer (Optional) - Duration of the voice message in seconds :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.voice = voice self.caption = caption self.duration = duration self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -884,18 +967,19 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'caption': self.caption, 'duration': self.duration, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } -class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for send video note on to webhook. """ __slots__ = ('chat_id', 'video_note', 'duration', 'length', 'disable_notification', - 'reply_to_message_id', 'reply_markup') + 'protect_content', 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_VIDEO_NOTE @@ -904,6 +988,7 @@ class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin): duration: Optional[Integer] = None, length: Optional[Integer] = None, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None): @@ -917,16 +1002,22 @@ class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin): :param length: Integer (Optional) - Video width and height :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.video_note = video_note self.duration = duration self.length = length self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -937,23 +1028,26 @@ class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'duration': self.duration, 'length': self.length, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } -class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use this method to send a group of photos or videos as an album. """ - __slots__ = ('chat_id', 'media', 'disable_notification', 'reply_to_message_id') + __slots__ = ('chat_id', 'media', 'disable_notification', + 'protect_content', 'reply_to_message_id') method = api.Methods.SEND_MEDIA_GROUP def __init__(self, chat_id: Union[Integer, String], media: Union[types.MediaGroup, List] = None, disable_notification: typing.Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: typing.Optional[Integer] = None): """ Use this method to send a group of photos or videos as an album. @@ -966,6 +1060,8 @@ class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin): :type media: :obj:`typing.Union[types.MediaGroup, typing.List]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` :return: On success, an array of the sent Messages is returned. @@ -976,10 +1072,13 @@ class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin): elif isinstance(media, list): # Convert list to MediaGroup media = types.MediaGroup(media) + if protect_content is None: + protect_content = self._global_protect_content() self.chat_id = chat_id self.media = media self.disable_notifications = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id def prepare(self): @@ -993,6 +1092,7 @@ class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'chat_id': self.chat_id, 'media': media, 'disable_notifications': self.disable_notifications, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id } @@ -1023,18 +1123,20 @@ class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin): return self -class SendLocation(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendLocation(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for send location on to webhook. """ - __slots__ = ('chat_id', 'latitude', 'longitude', 'disable_notification', 'reply_to_message_id', 'reply_markup') + __slots__ = ('chat_id', 'latitude', 'longitude', 'disable_notification', + 'protect_content', 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_LOCATION def __init__(self, chat_id: Union[Integer, String], latitude: Float, longitude: Float, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None): @@ -1045,15 +1147,21 @@ class SendLocation(BaseResponse, ReplyToMixin, DisableNotificationMixin): :param longitude: Float - Longitude of location :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.latitude = latitude self.longitude = longitude self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -1063,18 +1171,21 @@ class SendLocation(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'latitude': self.latitude, 'longitude': self.longitude, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } -class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for send venue on to webhook. """ - __slots__ = ('chat_id', 'latitude', 'longitude', 'title', 'address', 'foursquare_id', - 'disable_notification', 'reply_to_message_id', 'reply_markup') + __slots__ = ('chat_id', 'latitude', 'longitude', 'title', + 'address', 'foursquare_id', + 'disable_notification', 'protect_content', + 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_VENUE @@ -1085,6 +1196,7 @@ class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin): address: String, foursquare_id: Optional[String] = None, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None): @@ -1098,11 +1210,16 @@ class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin): :param foursquare_id: String (Optional) - Foursquare identifier of the venue :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.latitude = latitude self.longitude = longitude @@ -1110,6 +1227,7 @@ class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin): self.address = address self.foursquare_id = foursquare_id self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -1122,18 +1240,19 @@ class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'address': self.address, 'foursquare_id': self.foursquare_id, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } -class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for send contact on to webhook. """ __slots__ = ('chat_id', 'phone_number', 'first_name', 'last_name', 'disable_notification', - 'reply_to_message_id', 'reply_markup') + 'protect_content', 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_CONTACT @@ -1142,6 +1261,7 @@ class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin): first_name: String, last_name: Optional[String] = None, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None): @@ -1153,16 +1273,22 @@ class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin): :param last_name: String (Optional) - Contact's last name :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove keyboard or to force a reply from the user. """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.phone_number = phone_number self.first_name = first_name self.last_name = last_name self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -1173,6 +1299,7 @@ class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'first_name': self.first_name, 'last_name': self.last_name, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } @@ -1730,18 +1857,20 @@ class DeleteMessage(BaseResponse): } -class SendSticker(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendSticker(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for send sticker on to webhook. """ - __slots__ = ('chat_id', 'sticker', 'disable_notification', 'reply_to_message_id', 'reply_markup') + __slots__ = ('chat_id', 'sticker', 'disable_notification', 'protect_content', + 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_STICKER def __init__(self, chat_id: Union[Integer, String], sticker: String, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[ Union[types.InlineKeyboardMarkup, @@ -1755,14 +1884,20 @@ class SendSticker(BaseResponse, ReplyToMixin, DisableNotificationMixin): or upload a new one using multipart/form-data. More info on Sending Files » :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.sticker = sticker self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -1771,6 +1906,7 @@ class SendSticker(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'chat_id': self.chat_id, 'sticker': self.sticker, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } @@ -1985,7 +2121,7 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'currency', 'prices', 'photo_url', 'photo_size', 'photo_width', 'photo_height', 'need_name', 'need_phone_number', 'need_email', 'need_shipping_address', 'send_phone_number_to_provider', 'send_email_to_provider', 'is_flexible', - 'disable_notification', 'reply_to_message_id', 'reply_markup') + 'disable_notification', 'protect_content', 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_INVOICE @@ -2009,6 +2145,7 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): send_email_to_provider: Optional[Boolean] = None, is_flexible: Optional[Boolean] = None, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[types.InlineKeyboardMarkup] = None): """ @@ -2042,6 +2179,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): :param is_flexible: Boolean (Optional) - Pass True, if the final price depends on the shipping method :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: types.InlineKeyboardMarkup (Optional) - A JSON-serialized object for an inline keyboard. If empty, one 'Pay total price' button will be shown. If not empty, the first button must be a Pay button. @@ -2066,6 +2205,7 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): self.send_email_to_provider = send_email_to_provider self.is_flexible = is_flexible self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -2091,6 +2231,7 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'send_email_to_provider': self.send_email_to_provider, 'is_flexible': self.is_flexible, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } @@ -2168,18 +2309,20 @@ class AnswerPreCheckoutQuery(BaseResponse): } -class SendGame(BaseResponse, ReplyToMixin, DisableNotificationMixin): +class SendGame(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectContentMixin): """ Use that response type for send game on to webhook. """ - __slots__ = ('chat_id', 'game_short_name', 'disable_notification', 'reply_to_message_id', 'reply_markup') + __slots__ = ('chat_id', 'game_short_name', 'disable_notification', + 'protect_content', 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_GAME def __init__(self, chat_id: Integer, game_short_name: String, disable_notification: Optional[Boolean] = None, + protect_content: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[types.InlineKeyboardMarkup] = None): """ @@ -2188,13 +2331,19 @@ class SendGame(BaseResponse, ReplyToMixin, DisableNotificationMixin): Set up your games via Botfather. :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. + :param protect_content: Boolean (Optional) - Protects the contents of sent messages + from forwarding and saving :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message :param reply_markup: types.InlineKeyboardMarkup (Optional) - A JSON-serialized object for an inline keyboard. If empty, one ‘Play game_title’ button will be shown. If not empty, the first button must launch the game. """ + if protect_content is None: + protect_content = self._global_protect_content() + self.chat_id = chat_id self.game_short_name = game_short_name self.disable_notification = disable_notification + self.protect_content = protect_content self.reply_to_message_id = reply_to_message_id self.reply_markup = reply_markup @@ -2203,6 +2352,7 @@ class SendGame(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'chat_id': self.chat_id, 'game_short_name': self.game_short_name, 'disable_notification': self.disable_notification, + 'protect_content': self.protect_content, 'reply_to_message_id': self.reply_to_message_id, 'reply_markup': prepare_arg(self.reply_markup), } diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 9378b32b..2c272fef 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -11,6 +11,7 @@ from .bot_command_scope import BotCommandScope, BotCommandScopeAllChatAdministra from .callback_game import CallbackGame from .callback_query import CallbackQuery from .chat import Chat, ChatActions, ChatType +from .chat_administrator_rights import ChatAdministratorRights from .chat_invite_link import ChatInviteLink from .chat_join_request import ChatJoinRequest from .chat_location import ChatLocation @@ -48,6 +49,7 @@ from .labeled_price import LabeledPrice from .location import Location from .login_url import LoginUrl from .mask_position import MaskPosition +from .menu_button import MenuButton, MenuButtonCommands, MenuButtonWebApp, MenuButtonDefault from .message import ContentType, ContentTypes, Message, ParseMode from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged from .message_entity import MessageEntity, MessageEntityType @@ -64,6 +66,7 @@ from .pre_checkout_query import PreCheckoutQuery from .proximity_alert_triggered import ProximityAlertTriggered from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, KeyboardButtonPollType from .response_parameters import ResponseParameters +from .sent_web_app_message import SentWebAppMessage from .shipping_address import ShippingAddress from .shipping_option import ShippingOption from .shipping_query import ShippingQuery @@ -75,12 +78,18 @@ from .user import User from .user_profile_photos import UserProfilePhotos from .venue import Venue from .video import Video +from .video_chat_ended import VideoChatEnded +from .video_chat_participants_invited import VideoChatParticipantsInvited +from .video_chat_scheduled import VideoChatScheduled +from .video_chat_started import VideoChatStarted from .video_note import VideoNote from .voice import Voice from .voice_chat_ended import VoiceChatEnded from .voice_chat_participants_invited import VoiceChatParticipantsInvited from .voice_chat_scheduled import VoiceChatScheduled from .voice_chat_started import VoiceChatStarted +from .web_app_data import WebAppData +from .web_app_info import WebAppInfo from .webhook_info import WebhookInfo __all__ = ( @@ -102,6 +111,7 @@ __all__ = ( 'CallbackQuery', 'Chat', 'ChatActions', + 'ChatAdministratorRights', 'ChatInviteLink', 'ChatJoinRequest', 'ChatLocation', @@ -174,6 +184,10 @@ __all__ = ( 'Location', 'LoginUrl', 'MaskPosition', + 'MenuButton', + 'MenuButtonCommands', + 'MenuButtonWebApp', + 'MenuButtonDefault', 'MediaGroup', 'Message', 'MessageAutoDeleteTimerChanged', @@ -201,6 +215,7 @@ __all__ = ( 'ReplyKeyboardMarkup', 'ReplyKeyboardRemove', 'ResponseParameters', + 'SentWebAppMessage', 'ShippingAddress', 'ShippingOption', 'ShippingQuery', @@ -212,12 +227,18 @@ __all__ = ( 'UserProfilePhotos', 'Venue', 'Video', + 'VideoChatEnded', + 'VideoChatParticipantsInvited', + 'VideoChatScheduled', + 'VideoChatStarted', 'VideoNote', 'Voice', 'VoiceChatEnded', 'VoiceChatParticipantsInvited', 'VoiceChatScheduled', 'VoiceChatStarted', + 'WebAppData', + 'WebAppInfo', 'WebhookInfo', 'base', 'fields', diff --git a/aiogram/types/base.py b/aiogram/types/base.py index cca3d5f1..b5befd38 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -3,12 +3,14 @@ from __future__ import annotations import io import logging import typing +import warnings from typing import TypeVar from babel.support import LazyProxy from .fields import BaseField from ..utils import json +from ..utils.exceptions import AIOGramWarning from ..utils.mixins import ContextInstanceMixin if typing.TYPE_CHECKING: from ..bot.bot import Bot @@ -243,8 +245,10 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return self.props[key].set_value(self, value, self.conf.get('parent', self)) self.values[key] = value - # Log warning when Telegram silently adds new Fields - log.warning("Field '%s' doesn't exist in %s", key, self.__class__) + # Show warning when Telegram silently adds new Fields + warnings.warn(f"Bot API Field {key!r} is not defined in {self.__class__!r} class.\n" + "Bot API has been updated. Check for updates at https://telegram.org/blog and " + "https://github.com/aiogram/aiogram/releases", AIOGramWarning) def __contains__(self, item: str) -> bool: """ diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index c18ad88b..506856cd 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -31,6 +31,9 @@ class Chat(base.TelegramObject): photo: ChatPhoto = fields.Field(base=ChatPhoto) bio: base.String = fields.Field() has_private_forwards: base.Boolean = fields.Field() + has_restricted_voice_and_video_messages: base.Boolean = fields.Field() + join_to_send_messages: base.Boolean = fields.Field() + join_by_request: base.Boolean = fields.Field() description: base.String = fields.Field() invite_link: base.String = fields.Field() pinned_message: 'Message' = fields.Field(base='Message') @@ -652,6 +655,7 @@ class ChatType(helper.Helper): """ List of chat types + :key: SENDER :key: PRIVATE :key: GROUP :key: SUPER_GROUP @@ -661,6 +665,7 @@ class ChatType(helper.Helper): mode = helper.HelperMode.lowercase + SENDER = helper.Item() # sender PRIVATE = helper.Item() # private GROUP = helper.Item() # group SUPERGROUP = helper.Item() # supergroup diff --git a/aiogram/types/chat_administrator_rights.py b/aiogram/types/chat_administrator_rights.py new file mode 100644 index 00000000..4e909db8 --- /dev/null +++ b/aiogram/types/chat_administrator_rights.py @@ -0,0 +1,46 @@ +from . import base +from . import fields + + +class ChatAdministratorRights(base.TelegramObject): + """ + Represents rights of an administrator in a chat. + + Source: https://core.telegram.org/bots/api#chatadministratorrights + """ + is_anonymous: base.Boolean = fields.Field() + can_manage_chat: base.Boolean = fields.Field() + can_delete_messages: base.Boolean = fields.Field() + can_manage_video_chats: base.Boolean = fields.Field() + can_restrict_members: base.Boolean = fields.Field() + can_promote_members: base.Boolean = fields.Field() + can_change_info: base.Boolean = fields.Field() + can_invite_users: base.Boolean = fields.Field() + can_post_messages: base.Boolean = fields.Field() + can_edit_messages: base.Boolean = fields.Field() + can_pin_messages: base.Boolean = fields.Field() + + def __init__(self, + is_anonymous: base.Boolean = None, + can_manage_chat: base.Boolean = None, + can_delete_messages: base.Boolean = None, + can_manage_video_chats: base.Boolean = None, + can_restrict_members: base.Boolean = None, + can_promote_members: base.Boolean = None, + can_change_info: base.Boolean = None, + can_invite_users: base.Boolean = None, + can_post_messages: base.Boolean = None, + can_edit_messages: base.Boolean = None, + can_pin_messages: base.Boolean = None): + super(ChatAdministratorRights, self).__init__( + is_anonymous=is_anonymous, + can_manage_chat=can_manage_chat, + can_delete_messages=can_delete_messages, + can_manage_video_chats=can_manage_video_chats, + can_restrict_members=can_restrict_members, + can_promote_members=can_promote_members, + can_change_info=can_change_info, + can_invite_users=can_invite_users, + can_post_messages=can_post_messages, + can_edit_messages=can_edit_messages, + can_pin_messages=can_pin_messages) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index ecbf9d2c..689d7f46 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -5,7 +5,6 @@ from . import base, fields from .user import User from ..utils import helper - T = typing.TypeVar('T') @@ -153,6 +152,7 @@ class ChatMemberAdministrator(ChatMember): can_edit_messages: base.Boolean = fields.Field() can_delete_messages: base.Boolean = fields.Field() can_manage_voice_chats: base.Boolean = fields.Field() + can_manage_video_chats: base.Boolean = fields.Field() can_restrict_members: base.Boolean = fields.Field() can_promote_members: base.Boolean = fields.Field() can_change_info: base.Boolean = fields.Field() diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py index d7a1d8ca..a0adea65 100644 --- a/aiogram/types/fields.py +++ b/aiogram/types/fields.py @@ -1,6 +1,7 @@ import abc import datetime import weakref +import sys __all__ = ('BaseField', 'Field', 'ListField', 'DateTimeField', 'TextField', 'ListOfLists', 'ConstField') @@ -168,8 +169,13 @@ class DateTimeField(Field): out: datetime """ - def serialize(self, value: datetime.datetime): - return round(value.timestamp()) + if sys.platform == "win32": + def serialize(self, value: datetime.datetime): + return round((value - datetime.datetime(1970, 1, 1)).total_seconds()) + + else: + def serialize(self, value: datetime.datetime): + return round(value.timestamp()) def deserialize(self, value, parent=None): return datetime.datetime.fromtimestamp(value) diff --git a/aiogram/types/inline_keyboard.py b/aiogram/types/inline_keyboard.py index 195c1e67..16d97e58 100644 --- a/aiogram/types/inline_keyboard.py +++ b/aiogram/types/inline_keyboard.py @@ -4,6 +4,7 @@ from . import base from . import fields from .callback_game import CallbackGame from .login_url import LoginUrl +from .web_app_info import WebAppInfo class InlineKeyboardMarkup(base.TelegramObject): @@ -95,6 +96,7 @@ class InlineKeyboardButton(base.TelegramObject): switch_inline_query_current_chat: base.String = fields.Field() callback_game: CallbackGame = fields.Field(base=CallbackGame) pay: base.Boolean = fields.Field() + web_app: WebAppInfo = fields.Field(base=WebAppInfo) def __init__(self, text: base.String, url: base.String = None, @@ -103,7 +105,9 @@ class InlineKeyboardButton(base.TelegramObject): switch_inline_query: base.String = None, switch_inline_query_current_chat: base.String = None, callback_game: CallbackGame = None, - pay: base.Boolean = None, **kwargs): + pay: base.Boolean = None, + web_app: WebAppInfo = None, + **kwargs): super(InlineKeyboardButton, self).__init__(text=text, url=url, login_url=login_url, @@ -111,4 +115,6 @@ class InlineKeyboardButton(base.TelegramObject): switch_inline_query=switch_inline_query, switch_inline_query_current_chat=switch_inline_query_current_chat, callback_game=callback_game, - pay=pay, **kwargs) + pay=pay, + web_app=web_app, + **kwargs) diff --git a/aiogram/types/menu_button.py b/aiogram/types/menu_button.py new file mode 100644 index 00000000..1fd9cf30 --- /dev/null +++ b/aiogram/types/menu_button.py @@ -0,0 +1,86 @@ +import typing + +from . import base +from . import fields +from .web_app_info import WebAppInfo +from ..utils import helper +from ..utils.helper import Item + + +class MenuButton(base.TelegramObject): + """ + This object describes the bot's menu button in a private chat. It should be one of + + - MenuButtonCommands + - MenuButtonWebApp + - MenuButtonDefault + + If a menu button other than MenuButtonDefault is set for a private chat, + then it is applied in the chat. + Otherwise the default menu button is applied. + By default, the menu button opens the list of bot commands. + """ + type: base.String = fields.Field(default='default') + + @classmethod + def resolve(cls, **kwargs) -> typing.Union[ + "MenuButtonCommands", + "MenuButtonDefault", + "MenuButtonWebApp", + ]: + type_ = kwargs.get('type') + mapping = { + MenuButtonType.DEFAULT: MenuButtonDefault, + MenuButtonType.COMMANDS: MenuButtonCommands, + MenuButtonType.WEB_APP: MenuButtonWebApp, + } + class_ = mapping.get(type_) + if not class_: + raise ValueError(f'Unknown MenuButton type: {type_}') + return class_(**kwargs) + + +class MenuButtonCommands(MenuButton): + """ + Represents a menu button, which opens the bot's list of commands. + + Source: https://core.telegram.org/bots/api#menubuttoncommands + """ + type: base.String = fields.Field(default='commands') + + def __init__(self, **kwargs): + super().__init__(type='commands', **kwargs) + + +class MenuButtonWebApp(MenuButton): + """ + Represents a menu button, which launches a Web App. + + Source: https://core.telegram.org/bots/api#menubuttonwebapp + """ + type: base.String = fields.Field(default='web_app') + text: base.String = fields.Field() + web_app: WebAppInfo = fields.Field(base=WebAppInfo) + + def __init__(self, text: base.String, web_app: WebAppInfo, **kwargs): + super().__init__(type='web_app', text=text, web_app=web_app, **kwargs) + + +class MenuButtonDefault(MenuButton): + """ + Describes that no specific value for the menu button was set. + + Source: https://core.telegram.org/bots/api#menubuttondefault + """ + type: base.String = fields.Field(default='default') + + def __init__(self, **kwargs): + super().__init__(type='default', **kwargs) + + +class MenuButtonType(helper.Helper): + mode = helper.HelperMode.lowercase + + DEFAULT = Item() + COMMANDS = Item() + WEB_APP = Item() diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 10ef8776..57dcff44 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -30,12 +30,17 @@ from .successful_payment import SuccessfulPayment from .user import User from .venue import Venue from .video import Video +from .video_chat_ended import VideoChatEnded +from .video_chat_participants_invited import VideoChatParticipantsInvited +from .video_chat_scheduled import VideoChatScheduled +from .video_chat_started import VideoChatStarted from .video_note import VideoNote from .voice import Voice from .voice_chat_ended import VoiceChatEnded from .voice_chat_participants_invited import VoiceChatParticipantsInvited from .voice_chat_scheduled import VoiceChatScheduled from .voice_chat_started import VoiceChatStarted +from .web_app_data import WebAppData from ..utils import helper from ..utils import markdown as md from ..utils.text_decorations import html_decoration, markdown_decoration @@ -106,6 +111,11 @@ class Message(base.TelegramObject): voice_chat_ended: VoiceChatEnded = fields.Field(base=VoiceChatEnded) voice_chat_participants_invited: VoiceChatParticipantsInvited = fields.Field(base=VoiceChatParticipantsInvited) reply_markup: InlineKeyboardMarkup = fields.Field(base=InlineKeyboardMarkup) + web_app_data: WebAppData = fields.Field(base=WebAppData) + video_chat_scheduled: VideoChatScheduled = fields.Field(base=VideoChatScheduled) + video_chat_started: VideoChatStarted = fields.Field(base=VideoChatStarted) + video_chat_ended: VideoChatEnded = fields.Field(base=VideoChatEnded) + video_chat_participants_invited: VideoChatParticipantsInvited = fields.Field(base=VideoChatParticipantsInvited) @property @functools.lru_cache() @@ -178,6 +188,16 @@ class Message(base.TelegramObject): return ContentType.VOICE_CHAT_ENDED if self.voice_chat_participants_invited: return ContentType.VOICE_CHAT_PARTICIPANTS_INVITED + if self.web_app_data: + return ContentType.WEB_APP_DATA + if self.video_chat_scheduled: + return ContentType.VIDEO_CHAT_SCHEDULED + if self.video_chat_started: + return ContentType.VIDEO_CHAT_STARTED + if self.video_chat_ended: + return ContentType.VIDEO_CHAT_ENDED + if self.video_chat_participants_invited: + return ContentType.VIDEO_CHAT_PARTICIPANTS_INVITED return ContentType.UNKNOWN @@ -250,6 +270,15 @@ class Message(base.TelegramObject): return text_decorator.unparse(text, entities) + @property + def from_id(self) -> int: + """ + User id if sent by user or chat/channel id if sent on behalf of a channel or chat + + :return: int + """ + return self.sender_chat.id if self.sender_chat else self.from_user.id + @property def md_text(self) -> str: """ @@ -309,22 +338,22 @@ class Message(base.TelegramObject): return md.link(text, url) async def answer( - self, - text: base.String, - parse_mode: typing.Optional[base.String] = None, - entities: typing.Optional[typing.List[MessageEntity]] = None, - disable_web_page_preview: typing.Optional[base.Boolean] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + text: base.String, + parse_mode: typing.Optional[base.String] = None, + entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_web_page_preview: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Answer to this message @@ -379,22 +408,22 @@ class Message(base.TelegramObject): ) async def answer_photo( - self, - photo: typing.Union[base.InputFile, base.String], - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + photo: typing.Union[base.InputFile, base.String], + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send photos. @@ -451,26 +480,26 @@ class Message(base.TelegramObject): ) async def answer_audio( - self, - audio: typing.Union[base.InputFile, base.String], - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - duration: typing.Optional[base.Integer] = None, - performer: typing.Optional[base.String] = None, - title: typing.Optional[base.String] = None, - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + audio: typing.Union[base.InputFile, base.String], + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + duration: typing.Optional[base.Integer] = None, + performer: typing.Optional[base.String] = None, + title: typing.Optional[base.String] = None, + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display them in the music player. @@ -547,26 +576,26 @@ class Message(base.TelegramObject): ) async def answer_animation( - self, - animation: typing.Union[base.InputFile, base.String], - duration: typing.Optional[base.Integer] = None, - width: typing.Optional[base.Integer] = None, - height: typing.Optional[base.Integer] = None, - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + animation: typing.Union[base.InputFile, base.String], + duration: typing.Optional[base.Integer] = None, + width: typing.Optional[base.Integer] = None, + height: typing.Optional[base.Integer] = None, + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -645,24 +674,24 @@ class Message(base.TelegramObject): ) async def answer_document( - self, - document: typing.Union[base.InputFile, base.String], - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - disable_content_type_detection: typing.Optional[base.Boolean] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + document: typing.Union[base.InputFile, base.String], + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_content_type_detection: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send general files. On success, the sent Message is @@ -734,27 +763,27 @@ class Message(base.TelegramObject): ) async def answer_video( - self, - video: typing.Union[base.InputFile, base.String], - duration: typing.Optional[base.Integer] = None, - width: typing.Optional[base.Integer] = None, - height: typing.Optional[base.Integer] = None, - thumb: typing.Union[base.InputFile, base.String, None] = None, - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - supports_streaming: typing.Optional[base.Boolean] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + video: typing.Union[base.InputFile, base.String], + duration: typing.Optional[base.Integer] = None, + width: typing.Optional[base.Integer] = None, + height: typing.Optional[base.Integer] = None, + thumb: typing.Union[base.InputFile, base.String, None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + supports_streaming: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send video files, Telegram clients support mp4 videos @@ -833,23 +862,23 @@ class Message(base.TelegramObject): ) async def answer_voice( - self, - voice: typing.Union[base.InputFile, base.String], - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - duration: typing.Optional[base.Integer] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + voice: typing.Union[base.InputFile, base.String], + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + duration: typing.Optional[base.Integer] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display the file @@ -914,22 +943,22 @@ class Message(base.TelegramObject): ) async def answer_video_note( - self, - video_note: typing.Union[base.InputFile, base.String], - duration: typing.Optional[base.Integer] = None, - length: typing.Optional[base.Integer] = None, - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + video_note: typing.Union[base.InputFile, base.String], + duration: typing.Optional[base.Integer] = None, + length: typing.Optional[base.Integer] = None, + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. @@ -986,12 +1015,12 @@ class Message(base.TelegramObject): ) async def answer_media_group( - self, - media: typing.Union[MediaGroup, typing.List], - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply: base.Boolean = False, + self, + media: typing.Union[MediaGroup, typing.List], + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply: base.Boolean = False, ) -> typing.List[Message]: """ Use this method to send a group of photos, videos, documents or audios as @@ -1032,24 +1061,24 @@ class Message(base.TelegramObject): ) async def answer_location( - self, - latitude: base.Float, - longitude: base.Float, - live_period: typing.Optional[base.Integer] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - horizontal_accuracy: typing.Optional[base.Float] = None, - heading: typing.Optional[base.Integer] = None, - proximity_alert_radius: typing.Optional[base.Integer] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + latitude: base.Float, + longitude: base.Float, + live_period: typing.Optional[base.Integer] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + horizontal_accuracy: typing.Optional[base.Float] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send point on the map. @@ -1116,26 +1145,26 @@ class Message(base.TelegramObject): ) async def answer_venue( - self, - latitude: base.Float, - longitude: base.Float, - title: base.String, - address: base.String, - foursquare_id: typing.Optional[base.String] = None, - foursquare_type: typing.Optional[base.String] = None, - google_place_id: typing.Optional[base.String] = None, - google_place_type: typing.Optional[base.String] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + latitude: base.Float, + longitude: base.Float, + title: base.String, + address: base.String, + foursquare_id: typing.Optional[base.String] = None, + foursquare_type: typing.Optional[base.String] = None, + google_place_id: typing.Optional[base.String] = None, + google_place_type: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send information about a venue. @@ -1210,21 +1239,21 @@ class Message(base.TelegramObject): ) async def answer_contact( - self, - phone_number: base.String, - first_name: base.String, - last_name: typing.Optional[base.String] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + phone_number: base.String, + first_name: base.String, + last_name: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send phone contacts. @@ -1275,19 +1304,19 @@ class Message(base.TelegramObject): ) async def answer_sticker( - self, - sticker: typing.Union[base.InputFile, base.String], - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + sticker: typing.Union[base.InputFile, base.String], + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send .webp stickers. @@ -1330,30 +1359,30 @@ class Message(base.TelegramObject): ) async def answer_poll( - self, - question: base.String, - options: typing.List[base.String], - is_anonymous: typing.Optional[base.Boolean] = None, - type: typing.Optional[base.String] = None, - allows_multiple_answers: typing.Optional[base.Boolean] = None, - correct_option_id: typing.Optional[base.Integer] = None, - explanation: typing.Optional[base.String] = None, - explanation_parse_mode: typing.Optional[base.String] = None, - explanation_entities: typing.Optional[typing.List[MessageEntity]] = None, - open_period: typing.Optional[base.Integer] = None, - close_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None, - is_closed: typing.Optional[base.Boolean] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + question: base.String, + options: typing.List[base.String], + is_anonymous: typing.Optional[base.Boolean] = None, + type: typing.Optional[base.String] = None, + allows_multiple_answers: typing.Optional[base.Boolean] = None, + correct_option_id: typing.Optional[base.Integer] = None, + explanation: typing.Optional[base.String] = None, + explanation_parse_mode: typing.Optional[base.String] = None, + explanation_entities: typing.Optional[typing.List[MessageEntity]] = None, + open_period: typing.Optional[base.Integer] = None, + close_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None, + is_closed: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send a native poll. On success, the sent Message is @@ -1454,19 +1483,19 @@ class Message(base.TelegramObject): ) async def answer_dice( - self, - emoji: typing.Optional[base.String] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = False, + self, + emoji: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, ) -> Message: """ Use this method to send an animated emoji that will display a random value. @@ -1516,8 +1545,8 @@ class Message(base.TelegramObject): ) async def answer_chat_action( - self, - action: base.String, + self, + action: base.String, ) -> base.Boolean: """ Use this method when you need to tell the user that something is happening on the bot's side. @@ -1540,22 +1569,22 @@ class Message(base.TelegramObject): ) async def reply( - self, - text: base.String, - parse_mode: typing.Optional[base.String] = None, - entities: typing.Optional[typing.List[MessageEntity]] = None, - disable_web_page_preview: typing.Optional[base.Boolean] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + text: base.String, + parse_mode: typing.Optional[base.String] = None, + entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_web_page_preview: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Reply to this message @@ -1610,22 +1639,22 @@ class Message(base.TelegramObject): ) async def reply_photo( - self, - photo: typing.Union[base.InputFile, base.String], - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + photo: typing.Union[base.InputFile, base.String], + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send photos. @@ -1682,26 +1711,26 @@ class Message(base.TelegramObject): ) async def reply_audio( - self, - audio: typing.Union[base.InputFile, base.String], - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - duration: typing.Optional[base.Integer] = None, - performer: typing.Optional[base.String] = None, - title: typing.Optional[base.String] = None, - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + audio: typing.Union[base.InputFile, base.String], + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + duration: typing.Optional[base.Integer] = None, + performer: typing.Optional[base.String] = None, + title: typing.Optional[base.String] = None, + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display them in the music player. @@ -1778,26 +1807,26 @@ class Message(base.TelegramObject): ) async def reply_animation( - self, - animation: typing.Union[base.InputFile, base.String], - duration: typing.Optional[base.Integer] = None, - width: typing.Optional[base.Integer] = None, - height: typing.Optional[base.Integer] = None, - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + animation: typing.Union[base.InputFile, base.String], + duration: typing.Optional[base.Integer] = None, + width: typing.Optional[base.Integer] = None, + height: typing.Optional[base.Integer] = None, + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -1876,24 +1905,24 @@ class Message(base.TelegramObject): ) async def reply_document( - self, - document: typing.Union[base.InputFile, base.String], - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - disable_content_type_detection: typing.Optional[base.Boolean] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + document: typing.Union[base.InputFile, base.String], + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_content_type_detection: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send general files. On success, the sent Message is @@ -1965,27 +1994,27 @@ class Message(base.TelegramObject): ) async def reply_video( - self, - video: typing.Union[base.InputFile, base.String], - duration: typing.Optional[base.Integer] = None, - width: typing.Optional[base.Integer] = None, - height: typing.Optional[base.Integer] = None, - thumb: typing.Union[base.InputFile, base.String, None] = None, - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - supports_streaming: typing.Optional[base.Boolean] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + video: typing.Union[base.InputFile, base.String], + duration: typing.Optional[base.Integer] = None, + width: typing.Optional[base.Integer] = None, + height: typing.Optional[base.Integer] = None, + thumb: typing.Union[base.InputFile, base.String, None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + supports_streaming: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send video files, Telegram clients support mp4 videos @@ -2064,23 +2093,23 @@ class Message(base.TelegramObject): ) async def reply_voice( - self, - voice: typing.Union[base.InputFile, base.String], - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - duration: typing.Optional[base.Integer] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + voice: typing.Union[base.InputFile, base.String], + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + duration: typing.Optional[base.Integer] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display the file @@ -2145,22 +2174,22 @@ class Message(base.TelegramObject): ) async def reply_video_note( - self, - video_note: typing.Union[base.InputFile, base.String], - duration: typing.Optional[base.Integer] = None, - length: typing.Optional[base.Integer] = None, - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + video_note: typing.Union[base.InputFile, base.String], + duration: typing.Optional[base.Integer] = None, + length: typing.Optional[base.Integer] = None, + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. @@ -2217,12 +2246,12 @@ class Message(base.TelegramObject): ) async def reply_media_group( - self, - media: typing.Union[MediaGroup, typing.List], - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply: base.Boolean = True, + self, + media: typing.Union[MediaGroup, typing.List], + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply: base.Boolean = True, ) -> typing.List[Message]: """ Use this method to send a group of photos, videos, documents or audios as @@ -2263,23 +2292,23 @@ class Message(base.TelegramObject): ) async def reply_location( - self, - latitude: base.Float, - longitude: base.Float, - live_period: typing.Optional[base.Integer] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - horizontal_accuracy: typing.Optional[base.Float] = None, - heading: typing.Optional[base.Integer] = None, - proximity_alert_radius: typing.Optional[base.Integer] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + latitude: base.Float, + longitude: base.Float, + live_period: typing.Optional[base.Integer] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + horizontal_accuracy: typing.Optional[base.Float] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send point on the map. @@ -2341,26 +2370,26 @@ class Message(base.TelegramObject): ) async def reply_venue( - self, - latitude: base.Float, - longitude: base.Float, - title: base.String, - address: base.String, - foursquare_id: typing.Optional[base.String] = None, - foursquare_type: typing.Optional[base.String] = None, - google_place_id: typing.Optional[base.String] = None, - google_place_type: typing.Optional[base.String] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + latitude: base.Float, + longitude: base.Float, + title: base.String, + address: base.String, + foursquare_id: typing.Optional[base.String] = None, + foursquare_type: typing.Optional[base.String] = None, + google_place_id: typing.Optional[base.String] = None, + google_place_type: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send information about a venue. @@ -2435,21 +2464,21 @@ class Message(base.TelegramObject): ) async def reply_contact( - self, - phone_number: base.String, - first_name: base.String, - last_name: typing.Optional[base.String] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + phone_number: base.String, + first_name: base.String, + last_name: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send phone contacts. @@ -2500,30 +2529,30 @@ class Message(base.TelegramObject): ) async def reply_poll( - self, - question: base.String, - options: typing.List[base.String], - is_anonymous: typing.Optional[base.Boolean] = None, - type: typing.Optional[base.String] = None, - allows_multiple_answers: typing.Optional[base.Boolean] = None, - correct_option_id: typing.Optional[base.Integer] = None, - explanation: typing.Optional[base.String] = None, - explanation_parse_mode: typing.Optional[base.String] = None, - explanation_entities: typing.Optional[typing.List[MessageEntity]] = None, - open_period: typing.Optional[base.Integer] = None, - close_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None, - is_closed: typing.Optional[base.Boolean] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + question: base.String, + options: typing.List[base.String], + is_anonymous: typing.Optional[base.Boolean] = None, + type: typing.Optional[base.String] = None, + allows_multiple_answers: typing.Optional[base.Boolean] = None, + correct_option_id: typing.Optional[base.Integer] = None, + explanation: typing.Optional[base.String] = None, + explanation_parse_mode: typing.Optional[base.String] = None, + explanation_entities: typing.Optional[typing.List[MessageEntity]] = None, + open_period: typing.Optional[base.Integer] = None, + close_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None, + is_closed: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send a native poll. On success, the sent Message is @@ -2624,19 +2653,19 @@ class Message(base.TelegramObject): ) async def reply_sticker( - self, - sticker: typing.Union[base.InputFile, base.String], - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + sticker: typing.Union[base.InputFile, base.String], + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send .webp stickers. @@ -2679,19 +2708,19 @@ class Message(base.TelegramObject): ) async def reply_dice( - self, - emoji: typing.Optional[base.String] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, - None, - ] = None, - reply: base.Boolean = True, + self, + emoji: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, ) -> Message: """ Use this method to send an animated emoji that will display a random value. @@ -2741,10 +2770,10 @@ class Message(base.TelegramObject): ) async def forward( - self, - chat_id: typing.Union[base.Integer, base.String], - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, + self, + chat_id: typing.Union[base.Integer, base.String], + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> Message: """ Forward this message @@ -2773,12 +2802,12 @@ class Message(base.TelegramObject): ) async def edit_text( - self, - text: base.String, - parse_mode: typing.Optional[base.String] = None, - entities: typing.Optional[typing.List[MessageEntity]] = None, - disable_web_page_preview: typing.Optional[base.Boolean] = None, - reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + self, + text: base.String, + parse_mode: typing.Optional[base.String] = None, + entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_web_page_preview: typing.Optional[base.Boolean] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, ) -> typing.Union[Message, base.Boolean]: """ Use this method to edit text and game messages sent by the bot or via the bot (for inline bots). @@ -2817,11 +2846,11 @@ class Message(base.TelegramObject): ) async def edit_caption( - self, - caption: base.String, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + self, + caption: base.String, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, ) -> typing.Union[Message, base.Boolean]: """ Use this method to edit captions of messages sent by the bot or via the bot @@ -2857,9 +2886,9 @@ class Message(base.TelegramObject): ) async def edit_media( - self, - media: InputMedia, - reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + self, + media: InputMedia, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, ) -> typing.Union[Message, base.Boolean]: """ Use this method to edit audio, document, photo, or video messages. @@ -2889,7 +2918,7 @@ class Message(base.TelegramObject): ) async def edit_reply_markup( - self, reply_markup: typing.Optional[InlineKeyboardMarkup] = None + self, reply_markup: typing.Optional[InlineKeyboardMarkup] = None ) -> typing.Union[Message, base.Boolean]: """ Use this method to edit only the reply markup of messages sent by the bot or via the bot (for inline bots). @@ -2919,10 +2948,10 @@ class Message(base.TelegramObject): ) async def edit_live_location( - self, - latitude: base.Float, - longitude: base.Float, - reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + self, + latitude: base.Float, + longitude: base.Float, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, ) -> typing.Union[Message, base.Boolean]: """ Use this method to edit live location messages sent by the bot or via the bot (for inline bots). @@ -2950,7 +2979,7 @@ class Message(base.TelegramObject): ) async def stop_live_location( - self, reply_markup: typing.Optional[InlineKeyboardMarkup] = None + self, reply_markup: typing.Optional[InlineKeyboardMarkup] = None ) -> typing.Union[Message, base.Boolean]: """ Use this method to stop updating a live location message sent by the bot or via the bot @@ -2986,8 +3015,8 @@ class Message(base.TelegramObject): return await self.bot.delete_message(self.chat.id, self.message_id) async def pin( - self, - disable_notification: typing.Optional[base.Boolean] = None, + self, + disable_notification: typing.Optional[base.Boolean] = None, ) -> base.Boolean: """ Use this method to add a message to the list of pinned messages in a chat. @@ -3028,16 +3057,16 @@ class Message(base.TelegramObject): ) async def send_copy( - self: Message, - chat_id: typing.Union[str, int], - disable_notification: typing.Optional[bool] = None, - protect_content: typing.Optional[base.Boolean] = None, - disable_web_page_preview: typing.Optional[bool] = None, - reply_to_message_id: typing.Optional[int] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[ - InlineKeyboardMarkup, ReplyKeyboardMarkup, None - ] = None, + self: Message, + chat_id: typing.Union[str, int], + disable_notification: typing.Optional[bool] = None, + protect_content: typing.Optional[base.Boolean] = None, + disable_web_page_preview: typing.Optional[bool] = None, + reply_to_message_id: typing.Optional[int] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, ReplyKeyboardMarkup, None + ] = None, ) -> Message: """ Send copy of current message @@ -3148,19 +3177,19 @@ class Message(base.TelegramObject): raise TypeError("This type of message can't be copied.") async def copy_to( - self, - chat_id: typing.Union[base.Integer, base.String], - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - caption_entities: typing.Optional[typing.List[MessageEntity]] = None, - disable_notification: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, - reply_to_message_id: typing.Optional[base.Integer] = None, - allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, + self, + chat_id: typing.Union[base.Integer, base.String], + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, ) -> MessageId: return await self.bot.copy_message( chat_id=chat_id, @@ -3247,6 +3276,11 @@ class ContentType(helper.Helper): VOICE_CHAT_STARTED = helper.Item() # voice_chat_started VOICE_CHAT_ENDED = helper.Item() # voice_chat_ended VOICE_CHAT_PARTICIPANTS_INVITED = helper.Item() # voice_chat_participants_invited + WEB_APP_DATA = helper.Item() # web_app_data + VIDEO_CHAT_SCHEDULED = helper.Item() # video_chat_scheduled + VIDEO_CHAT_STARTED = helper.Item() # video_chat_started + VIDEO_CHAT_ENDED = helper.Item() # video_chat_ended + VIDEO_CHAT_PARTICIPANTS_INVITED = helper.Item() # video_chat_participants_invited UNKNOWN = helper.Item() # unknown ANY = helper.Item() # any @@ -3313,6 +3347,11 @@ class ContentTypes(helper.Helper): DELETE_CHAT_PHOTO = helper.ListItem() # delete_chat_photo GROUP_CHAT_CREATED = helper.ListItem() # group_chat_created PASSPORT_DATA = helper.ListItem() # passport_data + WEB_APP_DATA = helper.Item() # web_app_data + VIDEO_CHAT_SCHEDULED = helper.Item() # video_chat_scheduled + VIDEO_CHAT_STARTED = helper.Item() # video_chat_started + VIDEO_CHAT_ENDED = helper.Item() # video_chat_ended + VIDEO_CHAT_PARTICIPANTS_INVITED = helper.Item() # video_chat_participants_invited UNKNOWN = helper.ListItem() # unknown ANY = helper.ListItem() # any diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 9788af05..9c910256 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -1,9 +1,9 @@ import sys -from ..utils import helper, markdown -from ..utils.deprecated import deprecated from . import base, fields from .user import User +from ..utils import helper, markdown +from ..utils.deprecated import deprecated class MessageEntity(base.TelegramObject): @@ -19,16 +19,18 @@ class MessageEntity(base.TelegramObject): url: base.String = fields.Field() user: User = fields.Field(base=User) language: base.String = fields.Field() + custom_emoji_id: base.String = fields.Field() def __init__( - self, - type: base.String, - offset: base.Integer, - length: base.Integer, - url: base.String = None, - user: User = None, - language: base.String = None, - **kwargs + self, + type: base.String, + offset: base.Integer, + length: base.Integer, + url: base.String = None, + user: User = None, + language: base.String = None, + custom_emoji_id: base.String = None, + **kwargs ): super().__init__( type=type, @@ -37,6 +39,7 @@ class MessageEntity(base.TelegramObject): url=url, user=user, language=language, + custom_emoji_id=custom_emoji_id, **kwargs ) @@ -94,6 +97,8 @@ class MessageEntity(base.TelegramObject): return method(entity_text, self.url) if self.type == MessageEntityType.TEXT_MENTION and self.user: return self.user.get_mention(entity_text, as_html=as_html) + if self.type == MessageEntityType.CUSTOM_EMOJI and self.user: + return entity_text return entity_text @@ -118,6 +123,7 @@ class MessageEntityType(helper.Helper): :key: PRE :key: TEXT_LINK :key: TEXT_MENTION + :key: CUSTOM_EMOJI """ mode = helper.HelperMode.snake_case @@ -138,3 +144,4 @@ class MessageEntityType(helper.Helper): PRE = helper.Item() # pre - monowidth block TEXT_LINK = helper.Item() # text_link - for clickable text URLs TEXT_MENTION = helper.Item() # text_mention - for users without usernames + CUSTOM_EMOJI = helper.Item() # custom_emoji diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index 17b0a353..1a8609be 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -2,6 +2,7 @@ import typing from . import base from . import fields +from .web_app_info import WebAppInfo class KeyboardButtonPollType(base.TelegramObject): @@ -117,16 +118,19 @@ class KeyboardButton(base.TelegramObject): request_contact: base.Boolean = fields.Field() request_location: base.Boolean = fields.Field() request_poll: KeyboardButtonPollType = fields.Field() + web_app: WebAppInfo = fields.Field(base=WebAppInfo) def __init__(self, text: base.String, request_contact: base.Boolean = None, request_location: base.Boolean = None, request_poll: KeyboardButtonPollType = None, + web_app: WebAppInfo = None, **kwargs): super(KeyboardButton, self).__init__(text=text, request_contact=request_contact, request_location=request_location, request_poll=request_poll, + web_app=web_app, **kwargs) diff --git a/aiogram/types/sent_web_app_message.py b/aiogram/types/sent_web_app_message.py new file mode 100644 index 00000000..d48343e8 --- /dev/null +++ b/aiogram/types/sent_web_app_message.py @@ -0,0 +1,11 @@ +from . import base +from . import fields + + +class SentWebAppMessage(base.TelegramObject): + """ + Contains information about an inline message sent by a Web App on behalf of a user. + + Source: https://core.telegram.org/bots/api#sentwebappmessage + """ + inline_message_id: base.String = fields.Field() diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py index 20c162e8..b8e0c4ed 100644 --- a/aiogram/types/sticker.py +++ b/aiogram/types/sticker.py @@ -3,6 +3,7 @@ from . import fields from . import mixins from .mask_position import MaskPosition from .photo_size import PhotoSize +from .file import File class Sticker(base.TelegramObject, mixins.Downloadable): @@ -13,6 +14,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable): """ file_id: base.String = fields.Field() file_unique_id: base.String = fields.Field() + type: base.String = fields.Field() width: base.Integer = fields.Field() height: base.Integer = fields.Field() is_animated: base.Boolean = fields.Field() @@ -20,7 +22,9 @@ class Sticker(base.TelegramObject, mixins.Downloadable): thumb: PhotoSize = fields.Field(base=PhotoSize) emoji: base.String = fields.Field() set_name: base.String = fields.Field() + premium_animation: File = fields.Field(base=File) mask_position: MaskPosition = fields.Field(base=MaskPosition) + custom_emoji_id: base.String = fields.Field() file_size: base.Integer = fields.Field() async def set_position_in_set(self, position: base.Integer) -> base.Boolean: diff --git a/aiogram/types/sticker_set.py b/aiogram/types/sticker_set.py index dabae5db..809094c2 100644 --- a/aiogram/types/sticker_set.py +++ b/aiogram/types/sticker_set.py @@ -14,8 +14,9 @@ class StickerSet(base.TelegramObject): """ name: base.String = fields.Field() title: base.String = fields.Field() + sticker_type: base.String = fields.Field() is_animated: base.Boolean = fields.Field() is_video: base.Boolean = fields.Field() - contains_masks: base.Boolean = fields.Field() + contains_masks: base.Boolean = fields.Field() # Deprecated stickers: typing.List[Sticker] = fields.ListField(base=Sticker) thumb: PhotoSize = fields.Field(base=PhotoSize) diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 6bde2dcd..3d594ecc 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -22,6 +22,8 @@ class User(base.TelegramObject): last_name: base.String = fields.Field() username: base.String = fields.Field() language_code: base.String = fields.Field() + is_premium: base.Boolean = fields.Field() + added_to_attachment_menu: base.Boolean = fields.Field() can_join_groups: base.Boolean = fields.Field() can_read_all_group_messages: base.Boolean = fields.Field() supports_inline_queries: base.Boolean = fields.Field() diff --git a/aiogram/types/video_chat_ended.py b/aiogram/types/video_chat_ended.py new file mode 100644 index 00000000..b73bf0d3 --- /dev/null +++ b/aiogram/types/video_chat_ended.py @@ -0,0 +1,13 @@ +from . import base +from . import fields +from . import mixins + + +class VideoChatEnded(base.TelegramObject, mixins.Downloadable): + """ + This object represents a service message about a video chat scheduled in the chat. + + https://core.telegram.org/bots/api#videochatended + """ + + duration: base.Integer = fields.Field() diff --git a/aiogram/types/video_chat_participants_invited.py b/aiogram/types/video_chat_participants_invited.py new file mode 100644 index 00000000..e7c33cb9 --- /dev/null +++ b/aiogram/types/video_chat_participants_invited.py @@ -0,0 +1,16 @@ +import typing + +from . import base +from . import fields +from . import mixins +from .user import User + + +class VideoChatParticipantsInvited(base.TelegramObject, mixins.Downloadable): + """ + This object represents a service message about new members invited to a video chat. + + https://core.telegram.org/bots/api#videochatparticipantsinvited + """ + + users: typing.List[User] = fields.ListField(base=User) diff --git a/aiogram/types/video_chat_scheduled.py b/aiogram/types/video_chat_scheduled.py new file mode 100644 index 00000000..375995e2 --- /dev/null +++ b/aiogram/types/video_chat_scheduled.py @@ -0,0 +1,14 @@ +from datetime import datetime + +from . import base +from . import fields + + +class VideoChatScheduled(base.TelegramObject): + """ + This object represents a service message about a video chat scheduled in the chat. + + https://core.telegram.org/bots/api#videochatscheduled + """ + + start_date: datetime = fields.DateTimeField() diff --git a/aiogram/types/video_chat_started.py b/aiogram/types/video_chat_started.py new file mode 100644 index 00000000..ec1aefd1 --- /dev/null +++ b/aiogram/types/video_chat_started.py @@ -0,0 +1,11 @@ +from . import base +from . import mixins + + +class VideoChatStarted(base.TelegramObject, mixins.Downloadable): + """ + his object represents a service message about a video chat started in the chat. Currently holds no information. + + https://core.telegram.org/bots/api#videochatstarted + """ + pass diff --git a/aiogram/types/web_app_data.py b/aiogram/types/web_app_data.py new file mode 100644 index 00000000..9c68d45e --- /dev/null +++ b/aiogram/types/web_app_data.py @@ -0,0 +1,12 @@ +from . import base +from . import fields + + +class WebAppData(base.TelegramObject): + """ + Contains data sent from a Web App to the bot. + + Source: https://core.telegram.org/bots/api#webappdata + """ + data: str = fields.Field() + button_text: str = fields.Field() diff --git a/aiogram/types/web_app_info.py b/aiogram/types/web_app_info.py new file mode 100644 index 00000000..31ac58b4 --- /dev/null +++ b/aiogram/types/web_app_info.py @@ -0,0 +1,11 @@ +from . import base +from . import fields + + +class WebAppInfo(base.TelegramObject): + """ + Contains information about a Web App. + + Source: https://core.telegram.org/bots/api#webappinfo + """ + url: base.String = fields.Field() diff --git a/aiogram/types/webhook_info.py b/aiogram/types/webhook_info.py index dc1a7cd9..8970c97a 100644 --- a/aiogram/types/webhook_info.py +++ b/aiogram/types/webhook_info.py @@ -18,3 +18,4 @@ class WebhookInfo(base.TelegramObject): last_error_message: base.String = fields.Field() max_connections: base.Integer = fields.Field() allowed_updates: typing.List[base.String] = fields.ListField() + last_synchronization_error_date: base.Integer = fields.DateTimeField() diff --git a/aiogram/utils/emoji.py b/aiogram/utils/emoji.py index 07faff56..92252076 100644 --- a/aiogram/utils/emoji.py +++ b/aiogram/utils/emoji.py @@ -1,3 +1,5 @@ +import warnings + try: import emoji except ImportError: @@ -5,8 +7,14 @@ except ImportError: def emojize(text): + warnings.warn(message="'aiogram.utils.emoji' module deprecated, use emoji symbols directly instead," + "this function will be removed in aiogram v2.22", + category=DeprecationWarning, stacklevel=2) return emoji.emojize(text, use_aliases=True) def demojize(text): + warnings.warn(message="'aiogram.utils.emoji' module deprecated, use emoji symbols directly instead," + "this function will be removed in aiogram v2.22", + category=DeprecationWarning, stacklevel=2) return emoji.demojize(text) diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index ae9af7d4..b7a8ae86 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -44,6 +44,8 @@ class TextDecoration(ABC): return self.link(value=text, link=f"tg://user?id={user.id}") if entity.type == "text_link": return self.link(value=text, link=cast(str, entity.url)) + if entity.type == "custom_emoji": + return self.custom_emoji(value=text, custom_emoji_id=entity.custom_emoji_id) return self.quote(text) @@ -143,6 +145,10 @@ class TextDecoration(ABC): def quote(self, value: str) -> str: # pragma: no cover pass + @abstractmethod + def custom_emoji(self, value: str, custom_emoji_id: str) -> str: # pragma: no cover + pass + class HtmlDecoration(TextDecoration): def link(self, value: str, link: str) -> str: @@ -175,6 +181,9 @@ class HtmlDecoration(TextDecoration): def quote(self, value: str) -> str: return html.escape(value, quote=False) + def custom_emoji(self, value: str, custom_emoji_id: str) -> str: + return f'{value}' + class MarkdownDecoration(TextDecoration): MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])") @@ -209,6 +218,9 @@ class MarkdownDecoration(TextDecoration): def quote(self, value: str) -> str: return re.sub(pattern=self.MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=value) + def custom_emoji(self, value: str, custom_emoji_id: str) -> str: + return self.link(value=value, link=f"tg://emoji?id={custom_emoji_id}") + html_decoration = HtmlDecoration() markdown_decoration = MarkdownDecoration() diff --git a/aiogram/utils/web_app.py b/aiogram/utils/web_app.py new file mode 100644 index 00000000..1ba43658 --- /dev/null +++ b/aiogram/utils/web_app.py @@ -0,0 +1,67 @@ +import hashlib +import hmac +from operator import itemgetter +from typing import Callable, Any, Dict +from urllib.parse import parse_qsl + + +def check_webapp_signature(token: str, init_data: str) -> bool: + """ + Check incoming WebApp init data signature + + Source: https://core.telegram.org/bots/webapps#validating-data-received-via-the-web-app + + :param token: + :param init_data: + :return: + """ + try: + parsed_data = dict(parse_qsl(init_data)) + except ValueError: + # Init data is not a valid query string + return False + if "hash" not in parsed_data: + # Hash is not present in init data + return False + + hash_ = parsed_data.pop('hash') + data_check_string = "\n".join( + f"{k}={v}" for k, v in sorted(parsed_data.items(), key=itemgetter(0)) + ) + secret_key = hmac.new( + key=b"WebAppData", msg=token.encode(), digestmod=hashlib.sha256 + ) + calculated_hash = hmac.new( + key=secret_key.digest(), msg=data_check_string.encode(), digestmod=hashlib.sha256 + ).hexdigest() + return calculated_hash == hash_ + + +def parse_init_data(init_data: str, _loads: Callable[..., Any]) -> Dict[str, Any]: + """ + Parse WebApp init data and return it as dict + + :param init_data: + :param _loads: + :return: + """ + result = {} + for key, value in parse_qsl(init_data): + if (value.startswith('[') and value.endswith(']')) or (value.startswith('{') and value.endswith('}')): + value = _loads(value) + result[key] = value + return result + + +def safe_parse_webapp_init_data(token: str, init_data: str, _loads: Callable[..., Any]) -> Dict[str, Any]: + """ + Validate WebApp init data and return it as dict + + :param token: + :param init_data: + :param _loads: + :return: + """ + if check_webapp_signature(token, init_data): + return parse_init_data(init_data, _loads) + raise ValueError("Invalid init data signature") diff --git a/docs/source/index.rst b/docs/source/index.rst index 03292fd9..5cdc2efa 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.7-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.2-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/tests/test_dispatcher/test_filters/test_builtin.py b/tests/test_dispatcher/test_filters/test_builtin.py index 4f05cb22..35e6cb0a 100644 --- a/tests/test_dispatcher/test_filters/test_builtin.py +++ b/tests/test_dispatcher/test_filters/test_builtin.py @@ -1,4 +1,4 @@ -from typing import Set +from typing import Set, Union, Iterable from datetime import datetime import pytest @@ -6,9 +6,9 @@ import pytest from aiogram.dispatcher.filters.builtin import ( Text, extract_chat_ids, - ChatIDArgumentType, ForwardedMessageFilter, IDFilter, + ChatIDArgumentType, ForwardedMessageFilter, IDFilter, Command, ) -from aiogram.types import Message +from aiogram.types import Message, BotCommand from tests.types.dataset import MESSAGE, MESSAGE_FROM_CHANNEL @@ -108,3 +108,42 @@ class TestIDFilter: filter = IDFilter(chat_id=message_from_channel.chat.id) assert await filter.check(message_from_channel) + + +@pytest.mark.parametrize("command", [ + "/start", + "/start some args", +]) +@pytest.mark.parametrize("cmd_filter", [ + "start", + ("start",), + BotCommand(command="start", description="my desc"), + (BotCommand(command="start", description="bar"),), + (BotCommand(command="start", description="foo"), "help"), +]) +@pytest.mark.asyncio +async def test_commands_filter(command: str, cmd_filter: Union[Iterable[Union[str, BotCommand]], str, BotCommand]): + message_with_command = Message(**MESSAGE) + message_with_command.text = command + + start_filter = Command(commands=cmd_filter) + + assert await start_filter.check(message_with_command) + + +@pytest.mark.asyncio +async def test_commands_filter_not_checked(): + message_with_command = Message(**MESSAGE) + message_with_command.text = "/start" + + start_filter = Command(commands=["help", BotCommand("about", "my desc")]) + + assert not await start_filter.check(message_with_command) + + +def test_commands_filter_raises_error(): + with pytest.raises(ValueError): + start_filter = Command(commands=42) # noqa + with pytest.raises(ValueError): + start_filter = Command(commands=[42]) # noqa +